azalea_client/
mining.rs

1use azalea_block::{fluid_state::FluidState, Block, BlockState};
2use azalea_core::{direction::Direction, game_type::GameMode, position::BlockPos, tick::GameTick};
3use azalea_entity::{mining::get_mine_progress, FluidOnEyes, Physics};
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    interact::{
14        can_use_game_master_blocks, check_is_interaction_restricted, CurrentSequenceNumber,
15        HitResultComponent, SwingArmEvent,
16    },
17    inventory::{Inventory, InventorySet},
18    local_player::{LocalGameMode, PermissionLevel, PlayerAbilities},
19    movement::MoveEventsSet,
20    packet_handling::game::SendPacketEvent,
21    Client,
22};
23
24/// A plugin that allows clients to break blocks in the world.
25pub struct MinePlugin;
26impl Plugin for MinePlugin {
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#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
63pub struct MiningSet;
64
65impl Client {
66    pub fn start_mining(&mut self, position: BlockPos) {
67        self.ecs.lock().send_event(StartMiningBlockEvent {
68            entity: self.entity,
69            position,
70        });
71    }
72
73    /// When enabled, the bot will mine any block that it is looking at if it is
74    /// reachable.
75    pub fn left_click_mine(&self, enabled: bool) {
76        let mut ecs = self.ecs.lock();
77        let mut entity_mut = ecs.entity_mut(self.entity);
78
79        if enabled {
80            entity_mut.insert(LeftClickMine);
81        } else {
82            entity_mut.remove::<LeftClickMine>();
83        }
84    }
85}
86
87/// A component that simulates the client holding down left click to mine the
88/// block that it's facing, but this only interacts with blocks and not
89/// entities.
90#[derive(Component)]
91pub struct LeftClickMine;
92
93#[allow(clippy::type_complexity)]
94fn handle_auto_mine(
95    mut query: Query<
96        (
97            &HitResultComponent,
98            Entity,
99            Option<&Mining>,
100            &Inventory,
101            &MineBlockPos,
102            &MineItem,
103        ),
104        With<LeftClickMine>,
105    >,
106    mut start_mining_block_event: EventWriter<StartMiningBlockEvent>,
107    mut stop_mining_block_event: EventWriter<StopMiningBlockEvent>,
108) {
109    for (
110        hit_result_component,
111        entity,
112        mining,
113        inventory,
114        current_mining_pos,
115        current_mining_item,
116    ) in &mut query.iter_mut()
117    {
118        let block_pos = hit_result_component.block_pos;
119
120        if (mining.is_none()
121            || !is_same_mining_target(
122                block_pos,
123                inventory,
124                current_mining_pos,
125                current_mining_item,
126            ))
127            && !hit_result_component.miss
128        {
129            start_mining_block_event.send(StartMiningBlockEvent {
130                entity,
131                position: block_pos,
132            });
133        } else if mining.is_some() && hit_result_component.miss {
134            stop_mining_block_event.send(StopMiningBlockEvent { entity });
135        }
136    }
137}
138
139/// Information about the block we're currently mining. This is only present if
140/// we're currently mining a block.
141#[derive(Component)]
142pub struct Mining {
143    pub pos: BlockPos,
144    pub dir: Direction,
145}
146
147/// Start mining the block at the given position.
148///
149/// If we're looking at the block then the correct direction will be used,
150/// otherwise it'll be [`Direction::Down`].
151#[derive(Event)]
152pub struct StartMiningBlockEvent {
153    pub entity: Entity,
154    pub position: BlockPos,
155}
156fn handle_start_mining_block_event(
157    mut events: EventReader<StartMiningBlockEvent>,
158    mut start_mining_events: EventWriter<StartMiningBlockWithDirectionEvent>,
159    mut query: Query<&HitResultComponent>,
160) {
161    for event in events.read() {
162        let hit_result = query.get_mut(event.entity).unwrap();
163        let direction = if hit_result.block_pos == event.position {
164            // we're looking at the block
165            hit_result.direction
166        } else {
167            // we're not looking at the block, arbitrary direction
168            Direction::Down
169        };
170        start_mining_events.send(StartMiningBlockWithDirectionEvent {
171            entity: event.entity,
172            position: event.position,
173            direction,
174        });
175    }
176}
177
178#[derive(Event)]
179pub struct StartMiningBlockWithDirectionEvent {
180    pub entity: Entity,
181    pub position: BlockPos,
182    pub direction: Direction,
183}
184#[allow(clippy::too_many_arguments, clippy::type_complexity)]
185fn handle_start_mining_block_with_direction_event(
186    mut events: EventReader<StartMiningBlockWithDirectionEvent>,
187    mut finish_mining_events: EventWriter<FinishMiningBlockEvent>,
188    mut send_packet_events: EventWriter<SendPacketEvent>,
189    mut attack_block_events: EventWriter<AttackBlockEvent>,
190    mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
191    mut query: Query<(
192        &InstanceName,
193        &LocalGameMode,
194        &Inventory,
195        &FluidOnEyes,
196        &Physics,
197        Option<&Mining>,
198        &mut CurrentSequenceNumber,
199        &mut MineDelay,
200        &mut MineProgress,
201        &mut MineTicks,
202        &mut MineItem,
203        &mut MineBlockPos,
204    )>,
205    instances: Res<InstanceContainer>,
206    mut commands: Commands,
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                send_packet_events.send(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            send_packet_events.send(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 send_packet_events: EventWriter<SendPacketEvent>,
486    mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
487    mut query: Query<(&mut Mining, &MineBlockPos, &mut MineProgress)>,
488    mut commands: Commands,
489) {
490    for event in events.read() {
491        let (mut _mining, mine_block_pos, mut mine_progress) = query.get_mut(event.entity).unwrap();
492
493        let mine_block_pos =
494            mine_block_pos.expect("IsMining is true so MineBlockPos must be present");
495        send_packet_events.send(SendPacketEvent::new(
496            event.entity,
497            ServerboundPlayerAction {
498                action: s_player_action::Action::AbortDestroyBlock,
499                pos: mine_block_pos,
500                direction: Direction::Down,
501                sequence: 0,
502            },
503        ));
504        commands.entity(event.entity).remove::<Mining>();
505        **mine_progress = 0.;
506        mine_block_progress_events.send(MineBlockProgressEvent {
507            entity: event.entity,
508            position: mine_block_pos,
509            destroy_stage: None,
510        });
511    }
512}
513
514#[allow(clippy::too_many_arguments, clippy::type_complexity)]
515pub fn continue_mining_block(
516    mut query: Query<(
517        Entity,
518        &InstanceName,
519        &LocalGameMode,
520        &Inventory,
521        &MineBlockPos,
522        &MineItem,
523        &FluidOnEyes,
524        &Physics,
525        &Mining,
526        &mut MineDelay,
527        &mut MineProgress,
528        &mut MineTicks,
529        &mut CurrentSequenceNumber,
530    )>,
531    mut send_packet_events: EventWriter<SendPacketEvent>,
532    mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
533    mut finish_mining_events: EventWriter<FinishMiningBlockEvent>,
534    mut start_mining_events: EventWriter<StartMiningBlockWithDirectionEvent>,
535    mut swing_arm_events: EventWriter<SwingArmEvent>,
536    instances: Res<InstanceContainer>,
537    mut commands: Commands,
538) {
539    for (
540        entity,
541        instance_name,
542        game_mode,
543        inventory,
544        current_mining_pos,
545        current_mining_item,
546        fluid_on_eyes,
547        physics,
548        mining,
549        mut mine_delay,
550        mut mine_progress,
551        mut mine_ticks,
552        mut sequence_number,
553    ) in query.iter_mut()
554    {
555        if **mine_delay > 0 {
556            **mine_delay -= 1;
557            continue;
558        }
559
560        if game_mode.current == GameMode::Creative {
561            // TODO: worldborder check
562            **mine_delay = 5;
563            finish_mining_events.send(FinishMiningBlockEvent {
564                entity,
565                position: mining.pos,
566            });
567            *sequence_number += 1;
568            send_packet_events.send(SendPacketEvent::new(
569                entity,
570                ServerboundPlayerAction {
571                    action: s_player_action::Action::StartDestroyBlock,
572                    pos: mining.pos,
573                    direction: mining.dir,
574                    sequence: **sequence_number,
575                },
576            ));
577            swing_arm_events.send(SwingArmEvent { entity });
578        } else if is_same_mining_target(
579            mining.pos,
580            inventory,
581            current_mining_pos,
582            current_mining_item,
583        ) {
584            let instance_lock = instances.get(instance_name).unwrap();
585            let instance = instance_lock.read();
586            let target_block_state = instance.get_block_state(&mining.pos).unwrap_or_default();
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                *sequence_number += 1;
609                finish_mining_events.send(FinishMiningBlockEvent {
610                    entity,
611                    position: mining.pos,
612                });
613                send_packet_events.send(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,
620                    },
621                ));
622                **mine_progress = 0.;
623                **mine_ticks = 0.;
624                **mine_delay = 0;
625            }
626
627            mine_block_progress_events.send(MineBlockProgressEvent {
628                entity,
629                position: mining.pos,
630                destroy_stage: mine_progress.destroy_stage(),
631            });
632            swing_arm_events.send(SwingArmEvent { entity });
633        } else {
634            start_mining_events.send(StartMiningBlockWithDirectionEvent {
635                entity,
636                position: mining.pos,
637                direction: mining.dir,
638            });
639        }
640
641        swing_arm_events.send(SwingArmEvent { entity });
642    }
643}