Skip to main content

azalea_client/plugins/
mining.rs

1use azalea_block::{BlockState, BlockTrait, fluid_state::FluidState};
2use azalea_core::{direction::Direction, game_type::GameMode, position::BlockPos, tick::GameTick};
3use azalea_entity::{
4    ActiveEffects, Attributes, FluidOnEyes, Physics, PlayerAbilities, Position,
5    inventory::Inventory, mining::get_mine_progress,
6};
7use azalea_inventory::ItemStack;
8use azalea_physics::{PhysicsSystems, collision::BlockWithShape};
9use azalea_protocol::packets::game::s_player_action::{self, ServerboundPlayerAction};
10use azalea_registry::builtin::{BlockKind, ItemKind};
11use azalea_world::{WorldName, Worlds};
12use bevy_app::{App, Plugin, Update};
13use bevy_ecs::prelude::*;
14use derive_more::{Deref, DerefMut};
15use tracing::{debug, trace, warn};
16
17use crate::{
18    interact::{
19        BlockStatePredictionHandler, SwingArmEvent, can_use_game_master_blocks,
20        check_is_interaction_restricted, pick::HitResultComponent,
21    },
22    inventory::InventorySystems,
23    local_player::{LocalGameMode, PermissionLevel, WorldHolder},
24    movement::MoveEventsSystems,
25    packet::game::SendGamePacketEvent,
26};
27
28/// A plugin that allows clients to break blocks in the world.
29pub struct MiningPlugin;
30impl Plugin for MiningPlugin {
31    fn build(&self, app: &mut App) {
32        app.add_message::<StartMiningBlockEvent>()
33            .add_message::<StopMiningBlockEvent>()
34            .add_message::<MineBlockProgressEvent>()
35            .add_message::<AttackBlockEvent>()
36            .add_systems(
37                GameTick,
38                (
39                    update_mining_component,
40                    handle_auto_mine,
41                    handle_mining_queued,
42                    decrement_mine_delay,
43                    continue_mining_block,
44                )
45                    .chain()
46                    .before(PhysicsSystems)
47                    .before(super::movement::send_position)
48                    .before(super::interact::handle_start_use_item_queued)
49                    .after(azalea_entity::update_fluid_on_eyes)
50                    .in_set(MiningSystems),
51            )
52            .add_systems(
53                Update,
54                (
55                    handle_start_mining_block_event,
56                    handle_stop_mining_block_event,
57                )
58                    .chain()
59                    .in_set(MiningSystems)
60                    .after(InventorySystems)
61                    .after(MoveEventsSystems)
62                    .after(crate::interact::pick::update_hit_result_component)
63                    .after(crate::attack::handle_attack_event),
64            )
65            .add_observer(handle_finish_mining_block_observer);
66    }
67}
68
69/// The Bevy system set for things related to mining.
70#[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)]
71pub struct MiningSystems;
72
73/// A component that simulates the client holding down left click to mine the
74/// block that it's facing, but this only interacts with blocks and not
75/// entities.
76#[derive(Component)]
77pub struct LeftClickMine;
78
79#[allow(clippy::type_complexity)]
80fn handle_auto_mine(
81    mut query: Query<
82        (
83            &HitResultComponent,
84            Entity,
85            Option<&Mining>,
86            &Inventory,
87            &MineBlockPos,
88            &MineItem,
89        ),
90        With<LeftClickMine>,
91    >,
92    mut start_mining_block_event: MessageWriter<StartMiningBlockEvent>,
93    mut stop_mining_block_event: MessageWriter<StopMiningBlockEvent>,
94) {
95    for (
96        hit_result_component,
97        entity,
98        mining,
99        inventory,
100        current_mining_pos,
101        current_mining_item,
102    ) in &mut query.iter_mut()
103    {
104        let block_pos = hit_result_component
105            .as_block_hit_result_if_not_miss()
106            .map(|b| b.block_pos);
107
108        // start mining if we're looking at a block and we're not already mining it
109        if let Some(block_pos) = block_pos
110            && (mining.is_none()
111                || !is_same_mining_target(
112                    block_pos,
113                    inventory,
114                    current_mining_pos,
115                    current_mining_item,
116                ))
117        {
118            start_mining_block_event.write(StartMiningBlockEvent {
119                entity,
120                position: block_pos,
121                force: true,
122            });
123        } else if mining.is_some() && hit_result_component.miss() {
124            stop_mining_block_event.write(StopMiningBlockEvent { entity });
125        }
126    }
127}
128
129/// Information about the block we're currently mining.
130///
131/// This is only present if we're currently mining a block.
132#[derive(Clone, Component, Debug)]
133#[component(storage = "SparseSet")]
134pub struct Mining {
135    pub pos: BlockPos,
136    pub dir: Direction,
137    /// See [`MiningQueued::force`].
138    pub force: bool,
139}
140
141/// Start mining the block at the given position.
142///
143/// If we're looking at the block then the correct direction will be used,
144/// otherwise it'll be [`Direction::Down`].
145#[derive(Debug, Message)]
146pub struct StartMiningBlockEvent {
147    pub entity: Entity,
148    pub position: BlockPos,
149    /// Whether we should ignore blocks that are blocking the view of this
150    /// block.
151    ///
152    /// Most of the time, you'll want to set this to true as it'll make the
153    /// behavior more predictable. If it's set to false, then it might fail or
154    /// it might mine blocks other than the one at `position` (which may be
155    /// preferable if you're trying to act like vanilla).
156    pub force: bool,
157}
158fn handle_start_mining_block_event(
159    mut commands: Commands,
160    mut events: MessageReader<StartMiningBlockEvent>,
161    mut query: Query<&HitResultComponent>,
162) {
163    for event in events.read() {
164        trace!("{event:?}");
165        let hit_result = query.get_mut(event.entity).unwrap();
166        if event.force {
167            let direction = if let Some(block_hit_result) =
168                hit_result.as_block_hit_result_if_not_miss()
169                && block_hit_result.block_pos == event.position
170            {
171                // we're looking at the block
172                block_hit_result.direction
173            } else {
174                debug!(
175                    "Got StartMiningBlockEvent but we're not looking at the block ({hit_result:?}.block_pos != {:?}). Picking an arbitrary direction instead.",
176                    event.position
177                );
178                // we're not looking at the block, arbitrary direction
179                Direction::Down
180            };
181            commands.entity(event.entity).insert(MiningQueued {
182                position: event.position,
183                direction,
184                force: true,
185            });
186        } else {
187            // let block_hit_result = hit_result.as_block_hit_result_if_not_miss();
188            // let direction = block_hit_result.map_or(Direction::Down, |b| b.direction);
189            if let Some(block_hit_result) = hit_result.as_block_hit_result_if_not_miss()
190                && block_hit_result.block_pos == event.position
191            {
192                commands.entity(event.entity).insert(MiningQueued {
193                    position: event.position,
194                    direction: block_hit_result.direction,
195                    force: false,
196                });
197            } else {
198                warn!(
199                    "Got StartMiningBlockEvent with force=false but we're not looking at the block ({hit_result:?}.block_pos != {:?}). You should've looked at the block before trying to mine with force=false.",
200                    event.position
201                );
202            };
203        }
204    }
205}
206
207/// Present on entities when they're going to start mining a block next tick.
208#[derive(Clone, Component, Debug)]
209pub struct MiningQueued {
210    pub position: BlockPos,
211    pub direction: Direction,
212    /// Whether we should mine the block regardless of whether it's reachable.
213    pub force: bool,
214}
215#[allow(clippy::too_many_arguments, clippy::type_complexity)]
216pub fn handle_mining_queued(
217    mut commands: Commands,
218    mut attack_block_events: MessageWriter<AttackBlockEvent>,
219    mut mine_block_progress_events: MessageWriter<MineBlockProgressEvent>,
220    query: Query<(
221        Entity,
222        &MiningQueued,
223        &WorldHolder,
224        &LocalGameMode,
225        &Inventory,
226        &ActiveEffects,
227        &FluidOnEyes,
228        &Physics,
229        &Attributes,
230        Option<&mut Mining>,
231        &mut BlockStatePredictionHandler,
232        (
233            &mut MineDelay,
234            &mut MineProgress,
235            &mut MineTicks,
236            &mut MineItem,
237            &mut MineBlockPos,
238        ),
239    )>,
240) {
241    for (
242        entity,
243        mining_queued,
244        world_holder,
245        game_mode,
246        inventory,
247        active_effects,
248        fluid_on_eyes,
249        physics,
250        attributes,
251        mut mining,
252        mut sequence_number,
253        (
254            mut mine_delay,
255            mut mine_progress,
256            mut mine_ticks,
257            mut current_mining_item,
258            mut current_mining_pos,
259        ),
260    ) in query
261    {
262        trace!("handle_mining_queued {mining_queued:?}");
263        commands.entity(entity).remove::<MiningQueued>();
264
265        let world = world_holder.shared.read();
266        if check_is_interaction_restricted(
267            &world,
268            mining_queued.position,
269            &game_mode.current,
270            inventory,
271        ) {
272            continue;
273        }
274        // TODO (when world border is implemented): vanilla ignores if the block
275        // is outside of the worldborder
276
277        if let Some(mining) = &mut mining {
278            // this matters if we were previously mining a block without force
279            if mining_queued.force {
280                mining.force = true;
281            }
282        }
283
284        if game_mode.current == GameMode::Creative {
285            // In creative mode, first send START_DESTROY_BLOCK packet then immediately
286            // finish mining
287            commands.trigger(SendGamePacketEvent::new(
288                entity,
289                ServerboundPlayerAction {
290                    action: s_player_action::Action::StartDestroyBlock,
291                    pos: mining_queued.position,
292                    direction: mining_queued.direction,
293                    seq: sequence_number.start_predicting(),
294                },
295            ));
296            commands.trigger(FinishMiningBlockEvent {
297                entity,
298                position: mining_queued.position,
299            });
300            **mine_delay = 5;
301            commands.trigger(SwingArmEvent { entity });
302        } else if mining.is_none()
303            || !is_same_mining_target(
304                mining_queued.position,
305                inventory,
306                &current_mining_pos,
307                &current_mining_item,
308            )
309        {
310            if mining.is_some() {
311                // send a packet to stop mining since we just changed target
312                commands.trigger(SendGamePacketEvent::new(
313                    entity,
314                    ServerboundPlayerAction {
315                        action: s_player_action::Action::AbortDestroyBlock,
316                        pos: current_mining_pos
317                            .expect("IsMining is true so MineBlockPos must be present"),
318                        direction: mining_queued.direction,
319                        seq: 0,
320                    },
321                ));
322            }
323
324            let target_block_state = world
325                .get_block_state(mining_queued.position)
326                .unwrap_or_default();
327
328            // we can't break blocks if they don't have a bounding box
329            let block_is_solid = !target_block_state
330                .outline_shape(mining_queued.position)
331                .is_empty();
332
333            if block_is_solid && **mine_progress == 0. {
334                // interact with the block (like note block left click) here
335                attack_block_events.write(AttackBlockEvent {
336                    entity,
337                    position: mining_queued.position,
338                });
339            }
340
341            let block = Box::<dyn BlockTrait>::from(target_block_state);
342
343            let held_item = inventory.held_item();
344
345            if block_is_solid
346                && get_mine_progress(
347                    block.as_ref(),
348                    held_item,
349                    fluid_on_eyes,
350                    physics,
351                    attributes,
352                    active_effects,
353                ) >= 1.
354            {
355                // block was broken instantly (instamined)
356                commands.trigger(FinishMiningBlockEvent {
357                    entity,
358                    position: mining_queued.position,
359                });
360            } else {
361                let mining = Mining {
362                    pos: mining_queued.position,
363                    dir: mining_queued.direction,
364                    force: mining_queued.force,
365                };
366                trace!("inserting mining component {mining:?} for entity {entity:?}");
367                commands.entity(entity).insert(mining);
368                **current_mining_pos = Some(mining_queued.position);
369                **current_mining_item = held_item.clone();
370                **mine_progress = 0.;
371                **mine_ticks = 0.;
372                mine_block_progress_events.write(MineBlockProgressEvent {
373                    entity,
374                    position: mining_queued.position,
375                    destroy_stage: mine_progress.destroy_stage(),
376                });
377            }
378
379            commands.trigger(SendGamePacketEvent::new(
380                entity,
381                ServerboundPlayerAction {
382                    action: s_player_action::Action::StartDestroyBlock,
383                    pos: mining_queued.position,
384                    direction: mining_queued.direction,
385                    seq: sequence_number.start_predicting(),
386                },
387            ));
388            commands.trigger(SwingArmEvent { entity });
389            // another swing packet gets sent in the same tick in
390            // continue_mining_block, vanilla does this too
391        }
392    }
393}
394
395#[derive(Message)]
396pub struct MineBlockProgressEvent {
397    pub entity: Entity,
398    pub position: BlockPos,
399    pub destroy_stage: Option<u32>,
400}
401
402/// A player left clicked on a block, used for stuff like interacting with note
403/// blocks.
404#[derive(Message)]
405pub struct AttackBlockEvent {
406    pub entity: Entity,
407    pub position: BlockPos,
408}
409
410/// Returns whether the block and item are still the same as when we started
411/// mining.
412fn is_same_mining_target(
413    target_block: BlockPos,
414    inventory: &Inventory,
415    current_mining_pos: &MineBlockPos,
416    current_mining_item: &MineItem,
417) -> bool {
418    let held_item = inventory.held_item();
419    Some(target_block) == current_mining_pos.0 && held_item == &current_mining_item.0
420}
421
422/// A component bundle for players that can mine blocks.
423#[derive(Bundle, Clone, Default)]
424pub struct MineBundle {
425    pub delay: MineDelay,
426    pub progress: MineProgress,
427    pub ticks: MineTicks,
428    pub mining_pos: MineBlockPos,
429    pub mine_item: MineItem,
430}
431
432/// A component that counts down until we start mining the next block.
433#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
434pub struct MineDelay(pub u32);
435
436/// A component that stores the progress of the current mining operation.
437///
438/// This is a value between 0 and 1.
439#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
440pub struct MineProgress(pub f32);
441
442impl MineProgress {
443    pub fn destroy_stage(&self) -> Option<u32> {
444        if self.0 > 0. {
445            Some((self.0 * 10.) as u32)
446        } else {
447            None
448        }
449    }
450}
451
452/// A component that stores the number of ticks that we've been mining the same
453/// block for.
454///
455/// This is a float despite the fact that it should only ever be a round number.
456#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
457pub struct MineTicks(pub f32);
458
459/// A component that stores the position of the block we're currently mining.
460#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
461pub struct MineBlockPos(pub Option<BlockPos>);
462
463/// A component that contains the item we're currently using to mine, or
464/// [`ItemStack::Empty`] if nothing is being mined.
465#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
466pub struct MineItem(pub ItemStack);
467
468/// A trigger that's sent when we completed mining a block.
469#[derive(EntityEvent)]
470pub struct FinishMiningBlockEvent {
471    pub entity: Entity,
472    pub position: BlockPos,
473}
474
475pub fn handle_finish_mining_block_observer(
476    finish_mining_block: On<FinishMiningBlockEvent>,
477    mut query: Query<(
478        &WorldName,
479        &LocalGameMode,
480        &Inventory,
481        &PlayerAbilities,
482        &PermissionLevel,
483        &Position,
484        &mut BlockStatePredictionHandler,
485    )>,
486    worlds: Res<Worlds>,
487) {
488    let event = finish_mining_block.event();
489
490    let (
491        world_name,
492        game_mode,
493        inventory,
494        abilities,
495        permission_level,
496        player_pos,
497        mut prediction_handler,
498    ) = query.get_mut(finish_mining_block.entity).unwrap();
499    let world_lock = worlds.get(world_name).unwrap();
500    let world = world_lock.read();
501    if check_is_interaction_restricted(&world, event.position, &game_mode.current, inventory) {
502        return;
503    }
504
505    if game_mode.current == GameMode::Creative {
506        let held_item = inventory.held_item().kind();
507        if matches!(held_item, ItemKind::Trident | ItemKind::DebugStick)
508            || azalea_registry::tags::items::SWORDS.contains(&held_item)
509        {
510            return;
511        }
512    }
513
514    let Some(block_state) = world.get_block_state(event.position) else {
515        return;
516    };
517
518    let registry_block = block_state.as_block_kind();
519    if !can_use_game_master_blocks(abilities, permission_level)
520        && matches!(
521            registry_block,
522            BlockKind::CommandBlock | BlockKind::StructureBlock
523        )
524    {
525        return;
526    }
527    if block_state == BlockState::AIR {
528        return;
529    }
530
531    // when we break a waterlogged block we want to keep the water there
532    let fluid_state = FluidState::from(block_state);
533    let block_state_for_fluid = BlockState::from(fluid_state);
534    let old_state = world
535        .set_block_state(event.position, block_state_for_fluid)
536        .unwrap_or_default();
537    prediction_handler.retain_known_server_state(event.position, old_state, **player_pos);
538}
539
540/// Abort mining a block.
541#[derive(Message)]
542pub struct StopMiningBlockEvent {
543    pub entity: Entity,
544}
545pub fn handle_stop_mining_block_event(
546    mut events: MessageReader<StopMiningBlockEvent>,
547    mut commands: Commands,
548    mut mine_block_progress_events: MessageWriter<MineBlockProgressEvent>,
549    mut query: Query<(&MineBlockPos, &mut MineProgress)>,
550) {
551    for event in events.read() {
552        let (mine_block_pos, mut mine_progress) = query.get_mut(event.entity).unwrap();
553
554        let mine_block_pos =
555            mine_block_pos.expect("IsMining is true so MineBlockPos must be present");
556        commands.trigger(SendGamePacketEvent::new(
557            event.entity,
558            ServerboundPlayerAction {
559                action: s_player_action::Action::AbortDestroyBlock,
560                pos: mine_block_pos,
561                direction: Direction::Down,
562                seq: 0,
563            },
564        ));
565        commands.entity(event.entity).remove::<Mining>();
566        **mine_progress = 0.;
567        mine_block_progress_events.write(MineBlockProgressEvent {
568            entity: event.entity,
569            position: mine_block_pos,
570            destroy_stage: None,
571        });
572    }
573}
574
575pub fn decrement_mine_delay(mut query: Query<&mut MineDelay>) {
576    for mut mine_delay in &mut query {
577        if **mine_delay > 0 {
578            **mine_delay -= 1;
579        }
580    }
581}
582
583#[allow(clippy::too_many_arguments, clippy::type_complexity)]
584pub fn continue_mining_block(
585    mut query: Query<(
586        Entity,
587        &WorldName,
588        &LocalGameMode,
589        &Inventory,
590        &MineBlockPos,
591        &MineItem,
592        &ActiveEffects,
593        &FluidOnEyes,
594        &Physics,
595        &Attributes,
596        &Mining,
597        &mut MineDelay,
598        &mut MineProgress,
599        &mut MineTicks,
600        &mut BlockStatePredictionHandler,
601    )>,
602    mut commands: Commands,
603    mut mine_block_progress_events: MessageWriter<MineBlockProgressEvent>,
604    worlds: Res<Worlds>,
605) {
606    for (
607        entity,
608        world_name,
609        game_mode,
610        inventory,
611        current_mining_pos,
612        current_mining_item,
613        active_effects,
614        fluid_on_eyes,
615        physics,
616        attributes,
617        mining,
618        mut mine_delay,
619        mut mine_progress,
620        mut mine_ticks,
621        mut prediction_handler,
622    ) in query.iter_mut()
623    {
624        if game_mode.current == GameMode::Creative {
625            // TODO: worldborder check
626            **mine_delay = 5;
627            commands.trigger(SendGamePacketEvent::new(
628                entity,
629                ServerboundPlayerAction {
630                    action: s_player_action::Action::StartDestroyBlock,
631                    pos: mining.pos,
632                    direction: mining.dir,
633                    seq: prediction_handler.start_predicting(),
634                },
635            ));
636            commands.trigger(FinishMiningBlockEvent {
637                entity,
638                position: mining.pos,
639            });
640            commands.trigger(SwingArmEvent { entity });
641        } else if mining.force
642            || is_same_mining_target(
643                mining.pos,
644                inventory,
645                current_mining_pos,
646                current_mining_item,
647            )
648        {
649            trace!("continue mining block at {:?}", mining.pos);
650            let world_lock = worlds.get(world_name).unwrap();
651            let world = world_lock.read();
652            let target_block_state = world.get_block_state(mining.pos).unwrap_or_default();
653
654            trace!("target_block_state: {target_block_state:?}");
655
656            if target_block_state.is_air() {
657                commands.entity(entity).remove::<Mining>();
658                continue;
659            }
660            let block = Box::<dyn BlockTrait>::from(target_block_state);
661            **mine_progress += get_mine_progress(
662                block.as_ref(),
663                current_mining_item,
664                fluid_on_eyes,
665                physics,
666                attributes,
667                active_effects,
668            );
669
670            if **mine_ticks % 4. == 0. {
671                // vanilla makes a mining sound here
672            }
673            **mine_ticks += 1.;
674
675            if **mine_progress >= 1. {
676                // MiningQueued is removed in case we were doing an infinite loop that
677                // repeatedly inserts MiningQueued
678                commands.entity(entity).remove::<(Mining, MiningQueued)>();
679                trace!("finished mining block at {:?}", mining.pos);
680                commands.trigger(FinishMiningBlockEvent {
681                    entity,
682                    position: mining.pos,
683                });
684                commands.trigger(SendGamePacketEvent::new(
685                    entity,
686                    ServerboundPlayerAction {
687                        action: s_player_action::Action::StopDestroyBlock,
688                        pos: mining.pos,
689                        direction: mining.dir,
690                        seq: prediction_handler.start_predicting(),
691                    },
692                ));
693                **mine_progress = 0.;
694                **mine_ticks = 0.;
695                **mine_delay = 5;
696            }
697
698            mine_block_progress_events.write(MineBlockProgressEvent {
699                entity,
700                position: mining.pos,
701                destroy_stage: mine_progress.destroy_stage(),
702            });
703            commands.trigger(SwingArmEvent { entity });
704        } else {
705            trace!("switching mining target to {:?}", mining.pos);
706            commands.entity(entity).insert(MiningQueued {
707                position: mining.pos,
708                direction: mining.dir,
709                force: false,
710            });
711        }
712    }
713}
714
715pub fn update_mining_component(
716    mut commands: Commands,
717    mut query: Query<(Entity, &mut Mining, &HitResultComponent)>,
718) {
719    for (entity, mut mining, hit_result_component) in &mut query.iter_mut() {
720        if let Some(block_hit_result) = hit_result_component.as_block_hit_result_if_not_miss() {
721            if mining.force && block_hit_result.block_pos != mining.pos {
722                continue;
723            }
724
725            if mining.pos != block_hit_result.block_pos {
726                debug!(
727                    "Updating Mining::pos from {:?} to {:?}",
728                    mining.pos, block_hit_result.block_pos
729                );
730                mining.pos = block_hit_result.block_pos;
731            }
732            mining.dir = block_hit_result.direction;
733        } else {
734            if mining.force {
735                continue;
736            }
737
738            debug!("Removing mining component because we're no longer looking at the block");
739            commands.entity(entity).remove::<Mining>();
740        }
741    }
742}