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;
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,
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::<StartMiningBlockWithDirectionEvent>()
30            .add_event::<FinishMiningBlockEvent>()
31            .add_event::<StopMiningBlockEvent>()
32            .add_event::<MineBlockProgressEvent>()
33            .add_event::<AttackBlockEvent>()
34            .add_systems(
35                GameTick,
36                (continue_mining_block, handle_auto_mine)
37                    .chain()
38                    .before(PhysicsSet),
39            )
40            .add_systems(
41                Update,
42                (
43                    handle_start_mining_block_event,
44                    handle_start_mining_block_with_direction_event,
45                    handle_finish_mining_block_event,
46                    handle_stop_mining_block_event,
47                )
48                    .chain()
49                    .in_set(MiningSet)
50                    .after(InventorySet)
51                    .after(MoveEventsSet)
52                    .before(azalea_entity::update_bounding_box)
53                    .after(azalea_entity::update_fluid_on_eyes)
54                    .after(crate::interact::update_hit_result_component)
55                    .after(crate::attack::handle_attack_event)
56                    .after(crate::interact::handle_block_interact_event)
57                    .before(crate::interact::handle_swing_arm_event),
58            );
59    }
60}
61
62/// The Bevy system set for things related to mining.
63#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
64pub struct MiningSet;
65
66impl Client {
67    pub fn start_mining(&mut self, position: BlockPos) {
68        self.ecs.lock().send_event(StartMiningBlockEvent {
69            entity: self.entity,
70            position,
71        });
72    }
73
74    /// When enabled, the bot will mine any block that it is looking at if it is
75    /// reachable.
76    pub fn left_click_mine(&self, enabled: bool) {
77        let mut ecs = self.ecs.lock();
78        let mut entity_mut = ecs.entity_mut(self.entity);
79
80        if enabled {
81            entity_mut.insert(LeftClickMine);
82        } else {
83            entity_mut.remove::<LeftClickMine>();
84        }
85    }
86}
87
88/// A component that simulates the client holding down left click to mine the
89/// block that it's facing, but this only interacts with blocks and not
90/// entities.
91#[derive(Component)]
92pub struct LeftClickMine;
93
94#[allow(clippy::type_complexity)]
95fn handle_auto_mine(
96    mut query: Query<
97        (
98            &HitResultComponent,
99            Entity,
100            Option<&Mining>,
101            &Inventory,
102            &MineBlockPos,
103            &MineItem,
104        ),
105        With<LeftClickMine>,
106    >,
107    mut start_mining_block_event: EventWriter<StartMiningBlockEvent>,
108    mut stop_mining_block_event: EventWriter<StopMiningBlockEvent>,
109) {
110    for (
111        hit_result_component,
112        entity,
113        mining,
114        inventory,
115        current_mining_pos,
116        current_mining_item,
117    ) in &mut query.iter_mut()
118    {
119        let block_pos = hit_result_component.block_pos;
120
121        if (mining.is_none()
122            || !is_same_mining_target(
123                block_pos,
124                inventory,
125                current_mining_pos,
126                current_mining_item,
127            ))
128            && !hit_result_component.miss
129        {
130            start_mining_block_event.send(StartMiningBlockEvent {
131                entity,
132                position: block_pos,
133            });
134        } else if mining.is_some() && hit_result_component.miss {
135            stop_mining_block_event.send(StopMiningBlockEvent { entity });
136        }
137    }
138}
139
140/// Information about the block we're currently mining. This is only present if
141/// we're currently mining a block.
142#[derive(Component)]
143pub struct Mining {
144    pub pos: BlockPos,
145    pub dir: Direction,
146}
147
148/// Start mining the block at the given position.
149///
150/// If we're looking at the block then the correct direction will be used,
151/// otherwise it'll be [`Direction::Down`].
152#[derive(Event)]
153pub struct StartMiningBlockEvent {
154    pub entity: Entity,
155    pub position: BlockPos,
156}
157fn handle_start_mining_block_event(
158    mut events: EventReader<StartMiningBlockEvent>,
159    mut start_mining_events: EventWriter<StartMiningBlockWithDirectionEvent>,
160    mut query: Query<&HitResultComponent>,
161) {
162    for event in events.read() {
163        let hit_result = query.get_mut(event.entity).unwrap();
164        let direction = if hit_result.block_pos == event.position {
165            // we're looking at the block
166            hit_result.direction
167        } else {
168            // we're not looking at the block, arbitrary direction
169            Direction::Down
170        };
171        start_mining_events.send(StartMiningBlockWithDirectionEvent {
172            entity: event.entity,
173            position: event.position,
174            direction,
175        });
176    }
177}
178
179#[derive(Event)]
180pub struct StartMiningBlockWithDirectionEvent {
181    pub entity: Entity,
182    pub position: BlockPos,
183    pub direction: Direction,
184}
185#[allow(clippy::too_many_arguments, clippy::type_complexity)]
186fn handle_start_mining_block_with_direction_event(
187    mut events: EventReader<StartMiningBlockWithDirectionEvent>,
188    mut finish_mining_events: EventWriter<FinishMiningBlockEvent>,
189    mut commands: Commands,
190    mut attack_block_events: EventWriter<AttackBlockEvent>,
191    mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
192    mut query: Query<(
193        &InstanceName,
194        &LocalGameMode,
195        &Inventory,
196        &FluidOnEyes,
197        &Physics,
198        Option<&Mining>,
199        &mut CurrentSequenceNumber,
200        &mut MineDelay,
201        &mut MineProgress,
202        &mut MineTicks,
203        &mut MineItem,
204        &mut MineBlockPos,
205    )>,
206    instances: Res<InstanceContainer>,
207) {
208    for event in events.read() {
209        let (
210            instance_name,
211            game_mode,
212            inventory,
213            fluid_on_eyes,
214            physics,
215            mining,
216            mut sequence_number,
217            mut mine_delay,
218            mut mine_progress,
219            mut mine_ticks,
220            mut current_mining_item,
221            mut current_mining_pos,
222        ) = query.get_mut(event.entity).unwrap();
223
224        let instance_lock = instances.get(instance_name).unwrap();
225        let instance = instance_lock.read();
226        if check_is_interaction_restricted(
227            &instance,
228            &event.position,
229            &game_mode.current,
230            inventory,
231        ) {
232            continue;
233        }
234        // TODO (when world border is implemented): vanilla ignores if the block
235        // is outside of the worldborder
236
237        if game_mode.current == GameMode::Creative {
238            *sequence_number += 1;
239            finish_mining_events.send(FinishMiningBlockEvent {
240                entity: event.entity,
241                position: event.position,
242            });
243            **mine_delay = 5;
244        } else if mining.is_none()
245            || !is_same_mining_target(
246                event.position,
247                inventory,
248                &current_mining_pos,
249                &current_mining_item,
250            )
251        {
252            if mining.is_some() {
253                // send a packet to stop mining since we just changed target
254                commands.trigger(SendPacketEvent::new(
255                    event.entity,
256                    ServerboundPlayerAction {
257                        action: s_player_action::Action::AbortDestroyBlock,
258                        pos: current_mining_pos
259                            .expect("IsMining is true so MineBlockPos must be present"),
260                        direction: event.direction,
261                        sequence: 0,
262                    },
263                ));
264            }
265
266            let target_block_state = instance
267                .get_block_state(&event.position)
268                .unwrap_or_default();
269            *sequence_number += 1;
270            let target_registry_block = azalea_registry::Block::from(target_block_state);
271
272            // we can't break blocks if they don't have a bounding box
273
274            // TODO: So right now azalea doesn't differenciate between different types of
275            // bounding boxes. See ClipContext::block_shape for more info. Ideally this
276            // should just call ClipContext::block_shape and check if it's empty.
277            let block_is_solid = !target_block_state.is_air()
278                // this is a hack to make sure we can't break water or lava
279                && !matches!(
280                    target_registry_block,
281                    azalea_registry::Block::Water | azalea_registry::Block::Lava
282                );
283
284            if block_is_solid && **mine_progress == 0. {
285                // interact with the block (like note block left click) here
286                attack_block_events.send(AttackBlockEvent {
287                    entity: event.entity,
288                    position: event.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.send(FinishMiningBlockEvent {
307                    entity: event.entity,
308                    position: event.position,
309                });
310            } else {
311                commands.entity(event.entity).insert(Mining {
312                    pos: event.position,
313                    dir: event.direction,
314                });
315                **current_mining_pos = Some(event.position);
316                **current_mining_item = held_item;
317                **mine_progress = 0.;
318                **mine_ticks = 0.;
319                mine_block_progress_events.send(MineBlockProgressEvent {
320                    entity: event.entity,
321                    position: event.position,
322                    destroy_stage: mine_progress.destroy_stage(),
323                });
324            }
325
326            commands.trigger(SendPacketEvent::new(
327                event.entity,
328                ServerboundPlayerAction {
329                    action: s_player_action::Action::StartDestroyBlock,
330                    pos: event.position,
331                    direction: event.direction,
332                    sequence: **sequence_number,
333                },
334            ));
335        }
336    }
337}
338
339#[derive(Event)]
340pub struct MineBlockProgressEvent {
341    pub entity: Entity,
342    pub position: BlockPos,
343    pub destroy_stage: Option<u32>,
344}
345
346/// A player left clicked on a block, used for stuff like interacting with note
347/// blocks.
348#[derive(Event)]
349pub struct AttackBlockEvent {
350    pub entity: Entity,
351    pub position: BlockPos,
352}
353
354/// Returns whether the block and item are still the same as when we started
355/// mining.
356fn is_same_mining_target(
357    target_block: BlockPos,
358    inventory: &Inventory,
359    current_mining_pos: &MineBlockPos,
360    current_mining_item: &MineItem,
361) -> bool {
362    let held_item = inventory.held_item();
363    Some(target_block) == current_mining_pos.0 && held_item == current_mining_item.0
364}
365
366/// A component bundle for players that can mine blocks.
367#[derive(Bundle, Default)]
368pub struct MineBundle {
369    pub delay: MineDelay,
370    pub progress: MineProgress,
371    pub ticks: MineTicks,
372    pub mining_pos: MineBlockPos,
373    pub mine_item: MineItem,
374}
375
376/// A component that counts down until we start mining the next block.
377#[derive(Component, Debug, Default, Deref, DerefMut)]
378pub struct MineDelay(pub u32);
379
380/// A component that stores the progress of the current mining operation. This
381/// is a value between 0 and 1.
382#[derive(Component, Debug, Default, Deref, DerefMut)]
383pub struct MineProgress(pub f32);
384
385impl MineProgress {
386    pub fn destroy_stage(&self) -> Option<u32> {
387        if self.0 > 0. {
388            Some((self.0 * 10.) as u32)
389        } else {
390            None
391        }
392    }
393}
394
395/// A component that stores the number of ticks that we've been mining the same
396/// block for. This is a float even though it should only ever be a round
397/// number.
398#[derive(Component, Clone, Debug, Default, Deref, DerefMut)]
399pub struct MineTicks(pub f32);
400
401/// A component that stores the position of the block we're currently mining.
402#[derive(Component, Clone, Debug, Default, Deref, DerefMut)]
403pub struct MineBlockPos(pub Option<BlockPos>);
404
405/// A component that contains the item we're currently using to mine. If we're
406/// not mining anything, it'll be [`ItemStack::Empty`].
407#[derive(Component, Clone, Debug, Default, Deref, DerefMut)]
408pub struct MineItem(pub ItemStack);
409
410/// Sent when we completed mining a block.
411#[derive(Event)]
412pub struct FinishMiningBlockEvent {
413    pub entity: Entity,
414    pub position: BlockPos,
415}
416
417pub fn handle_finish_mining_block_event(
418    mut events: EventReader<FinishMiningBlockEvent>,
419    mut query: Query<(
420        &InstanceName,
421        &LocalGameMode,
422        &Inventory,
423        &PlayerAbilities,
424        &PermissionLevel,
425        &mut CurrentSequenceNumber,
426    )>,
427    instances: Res<InstanceContainer>,
428) {
429    for event in events.read() {
430        let (instance_name, game_mode, inventory, abilities, permission_level, _sequence_number) =
431            query.get_mut(event.entity).unwrap();
432        let instance_lock = instances.get(instance_name).unwrap();
433        let instance = instance_lock.read();
434        if check_is_interaction_restricted(
435            &instance,
436            &event.position,
437            &game_mode.current,
438            inventory,
439        ) {
440            continue;
441        }
442
443        if game_mode.current == GameMode::Creative {
444            let held_item = inventory.held_item().kind();
445            if matches!(
446                held_item,
447                azalea_registry::Item::Trident | azalea_registry::Item::DebugStick
448            ) || azalea_registry::tags::items::SWORDS.contains(&held_item)
449            {
450                continue;
451            }
452        }
453
454        let Some(block_state) = instance.get_block_state(&event.position) else {
455            continue;
456        };
457
458        let registry_block = Box::<dyn Block>::from(block_state).as_registry_block();
459        if !can_use_game_master_blocks(abilities, permission_level)
460            && matches!(
461                registry_block,
462                azalea_registry::Block::CommandBlock | azalea_registry::Block::StructureBlock
463            )
464        {
465            continue;
466        }
467        if block_state == BlockState::AIR {
468            continue;
469        }
470
471        // when we break a waterlogged block we want to keep the water there
472        let fluid_state = FluidState::from(block_state);
473        let block_state_for_fluid = BlockState::from(fluid_state);
474        instance.set_block_state(&event.position, block_state_for_fluid);
475    }
476}
477
478/// Abort mining a block.
479#[derive(Event)]
480pub struct StopMiningBlockEvent {
481    pub entity: Entity,
482}
483pub fn handle_stop_mining_block_event(
484    mut events: EventReader<StopMiningBlockEvent>,
485    mut commands: Commands,
486    mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
487    mut query: Query<(&mut Mining, &MineBlockPos, &mut MineProgress)>,
488) {
489    for event in events.read() {
490        let (mut _mining, mine_block_pos, mut mine_progress) = query.get_mut(event.entity).unwrap();
491
492        let mine_block_pos =
493            mine_block_pos.expect("IsMining is true so MineBlockPos must be present");
494        commands.trigger(SendPacketEvent::new(
495            event.entity,
496            ServerboundPlayerAction {
497                action: s_player_action::Action::AbortDestroyBlock,
498                pos: mine_block_pos,
499                direction: Direction::Down,
500                sequence: 0,
501            },
502        ));
503        commands.entity(event.entity).remove::<Mining>();
504        **mine_progress = 0.;
505        mine_block_progress_events.send(MineBlockProgressEvent {
506            entity: event.entity,
507            position: mine_block_pos,
508            destroy_stage: None,
509        });
510    }
511}
512
513#[allow(clippy::too_many_arguments, clippy::type_complexity)]
514pub fn continue_mining_block(
515    mut query: Query<(
516        Entity,
517        &InstanceName,
518        &LocalGameMode,
519        &Inventory,
520        &MineBlockPos,
521        &MineItem,
522        &FluidOnEyes,
523        &Physics,
524        &Mining,
525        &mut MineDelay,
526        &mut MineProgress,
527        &mut MineTicks,
528        &mut CurrentSequenceNumber,
529    )>,
530    mut commands: Commands,
531    mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
532    mut finish_mining_events: EventWriter<FinishMiningBlockEvent>,
533    mut start_mining_events: EventWriter<StartMiningBlockWithDirectionEvent>,
534    mut swing_arm_events: EventWriter<SwingArmEvent>,
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.send(FinishMiningBlockEvent {
562                entity,
563                position: mining.pos,
564            });
565            *sequence_number += 1;
566            commands.trigger(SendPacketEvent::new(
567                entity,
568                ServerboundPlayerAction {
569                    action: s_player_action::Action::StartDestroyBlock,
570                    pos: mining.pos,
571                    direction: mining.dir,
572                    sequence: **sequence_number,
573                },
574            ));
575            swing_arm_events.send(SwingArmEvent { entity });
576        } else if is_same_mining_target(
577            mining.pos,
578            inventory,
579            current_mining_pos,
580            current_mining_item,
581        ) {
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            if target_block_state.is_air() {
587                commands.entity(entity).remove::<Mining>();
588                continue;
589            }
590            let block = Box::<dyn Block>::from(target_block_state);
591            **mine_progress += get_mine_progress(
592                block.as_ref(),
593                current_mining_item.kind(),
594                &inventory.inventory_menu,
595                fluid_on_eyes,
596                physics,
597            );
598
599            if **mine_ticks % 4. == 0. {
600                // vanilla makes a mining sound here
601            }
602            **mine_ticks += 1.;
603
604            if **mine_progress >= 1. {
605                commands.entity(entity).remove::<Mining>();
606                *sequence_number += 1;
607                finish_mining_events.send(FinishMiningBlockEvent {
608                    entity,
609                    position: mining.pos,
610                });
611                commands.trigger(SendPacketEvent::new(
612                    entity,
613                    ServerboundPlayerAction {
614                        action: s_player_action::Action::StopDestroyBlock,
615                        pos: mining.pos,
616                        direction: mining.dir,
617                        sequence: **sequence_number,
618                    },
619                ));
620                **mine_progress = 0.;
621                **mine_ticks = 0.;
622                **mine_delay = 0;
623            }
624
625            mine_block_progress_events.send(MineBlockProgressEvent {
626                entity,
627                position: mining.pos,
628                destroy_stage: mine_progress.destroy_stage(),
629            });
630            swing_arm_events.send(SwingArmEvent { entity });
631        } else {
632            start_mining_events.send(StartMiningBlockWithDirectionEvent {
633                entity,
634                position: mining.pos,
635                direction: mining.dir,
636            });
637        }
638
639        swing_arm_events.send(SwingArmEvent { entity });
640    }
641}