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