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