azalea_client/plugins/
mining.rs

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