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