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