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