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