Skip to main content

azalea_client/plugins/
mining.rs

1use azalea_block::{BlockState, fluid_state::FluidState};
2use azalea_core::{direction::Direction, game_type::GameMode, position::BlockPos, tick::GameTick};
3use azalea_entity::{
4    ActiveEffects, Attributes, FluidOnEyes, Physics, PlayerAbilities, Position,
5    inventory::Inventory, mining::get_mine_progress,
6};
7use azalea_inventory::ItemStack;
8use azalea_physics::{PhysicsSystems, collision::BlockWithShape};
9use azalea_protocol::packets::game::s_player_action::{self, ServerboundPlayerAction};
10use azalea_registry::builtin::{BlockKind, ItemKind};
11use azalea_world::{WorldName, Worlds};
12use bevy_app::{App, Plugin, Update};
13use bevy_ecs::prelude::*;
14use derive_more::{Deref, DerefMut};
15use tracing::{debug, trace, warn};
16
17use crate::{
18    interact::{
19        BlockStatePredictionHandler, SwingArmEvent, can_use_game_master_blocks,
20        check_is_interaction_restricted, pick::HitResultComponent,
21    },
22    inventory::InventorySystems,
23    local_player::{PermissionLevel, WorldHolder},
24    movement::MoveEventsSystems,
25    packet::game::SendGamePacketEvent,
26};
27
28/// A plugin that allows clients to break blocks in the world.
29pub struct MiningPlugin;
30impl Plugin for MiningPlugin {
31    fn build(&self, app: &mut App) {
32        app.add_message::<StartMiningBlockEvent>()
33            .add_message::<StopMiningBlockEvent>()
34            .add_message::<MineBlockProgressEvent>()
35            .add_message::<AttackBlockEvent>()
36            .add_systems(
37                GameTick,
38                (
39                    update_mining_component,
40                    handle_auto_mine,
41                    handle_mining_queued,
42                    decrement_mine_delay,
43                    continue_mining_block,
44                )
45                    .chain()
46                    .before(PhysicsSystems)
47                    .before(super::movement::send_position)
48                    .before(super::interact::handle_start_use_item_queued)
49                    .after(azalea_entity::update_fluid_on_eyes)
50                    .in_set(MiningSystems),
51            )
52            .add_systems(
53                Update,
54                (
55                    handle_start_mining_block_event,
56                    handle_stop_mining_block_event,
57                )
58                    .chain()
59                    .in_set(MiningSystems)
60                    .after(InventorySystems)
61                    .after(MoveEventsSystems)
62                    .after(crate::interact::pick::update_hit_result_component)
63                    .after(crate::attack::handle_attack_event),
64            )
65            .add_observer(handle_finish_mining_block_observer);
66    }
67}
68
69/// The Bevy system set for things related to mining.
70#[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)]
71pub struct MiningSystems;
72
73/// A component that simulates the client holding down left click to mine the
74/// block that it's facing, but this only interacts with blocks and not
75/// entities.
76#[derive(Component)]
77pub struct LeftClickMine;
78
79#[allow(clippy::type_complexity)]
80fn handle_auto_mine(
81    mut query: Query<
82        (
83            &HitResultComponent,
84            Entity,
85            Option<&Mining>,
86            &Inventory,
87            &MineBlockPos,
88            &MineItem,
89        ),
90        With<LeftClickMine>,
91    >,
92    mut start_mining_block_event: MessageWriter<StartMiningBlockEvent>,
93    mut stop_mining_block_event: MessageWriter<StopMiningBlockEvent>,
94) {
95    for (
96        hit_result_component,
97        entity,
98        mining,
99        inventory,
100        current_mining_pos,
101        current_mining_item,
102    ) in &mut query.iter_mut()
103    {
104        let block_pos = hit_result_component
105            .as_block_hit_result_if_not_miss()
106            .map(|b| b.block_pos);
107
108        // start mining if we're looking at a block and we're not already mining it
109        if let Some(block_pos) = block_pos
110            && (mining.is_none()
111                || !is_same_mining_target(
112                    block_pos,
113                    inventory,
114                    current_mining_pos,
115                    current_mining_item,
116                ))
117        {
118            start_mining_block_event.write(StartMiningBlockEvent {
119                entity,
120                position: block_pos,
121                force: true,
122            });
123        } else if mining.is_some() && hit_result_component.miss() {
124            stop_mining_block_event.write(StopMiningBlockEvent { entity });
125        }
126    }
127}
128
129/// Information about the block we're currently mining.
130///
131/// This is only present if we're currently mining a block.
132#[derive(Clone, Component, Debug)]
133#[component(storage = "SparseSet")]
134pub struct Mining {
135    pub pos: BlockPos,
136    pub dir: Direction,
137    /// See [`MiningQueued::force`].
138    pub force: bool,
139}
140
141/// Start mining the block at the given position.
142///
143/// If we're looking at the block then the correct direction will be used,
144/// otherwise it'll be [`Direction::Down`].
145#[derive(Debug, Message)]
146pub struct StartMiningBlockEvent {
147    pub entity: Entity,
148    pub position: BlockPos,
149    /// Whether we should ignore blocks that are blocking the view of this
150    /// block.
151    ///
152    /// Most of the time, you'll want to set this to true as it'll make the
153    /// behavior more predictable. If it's set to false, then it might fail or
154    /// it might mine blocks other than the one at `position` (which may be
155    /// preferable if you're trying to act like vanilla).
156    pub force: bool,
157}
158fn handle_start_mining_block_event(
159    mut commands: Commands,
160    mut events: MessageReader<StartMiningBlockEvent>,
161    mut query: Query<&HitResultComponent>,
162) {
163    for event in events.read() {
164        trace!("{event:?}");
165        let hit_result = query.get_mut(event.entity).unwrap();
166        if event.force {
167            let direction = if let Some(block_hit_result) =
168                hit_result.as_block_hit_result_if_not_miss()
169                && block_hit_result.block_pos == event.position
170            {
171                // we're looking at the block
172                block_hit_result.direction
173            } else {
174                debug!(
175                    "Got StartMiningBlockEvent but we're not looking at the block ({hit_result:?}.block_pos != {:?}). Picking an arbitrary direction instead.",
176                    event.position
177                );
178                // we're not looking at the block, arbitrary direction
179                Direction::Down
180            };
181            commands.entity(event.entity).insert(MiningQueued {
182                position: event.position,
183                direction,
184                force: true,
185            });
186        } else {
187            // let block_hit_result = hit_result.as_block_hit_result_if_not_miss();
188            // let direction = block_hit_result.map_or(Direction::Down, |b| b.direction);
189            if let Some(block_hit_result) = hit_result.as_block_hit_result_if_not_miss()
190                && block_hit_result.block_pos == event.position
191            {
192                commands.entity(event.entity).insert(MiningQueued {
193                    position: event.position,
194                    direction: block_hit_result.direction,
195                    force: false,
196                });
197            } else {
198                warn!(
199                    "Got StartMiningBlockEvent with force=false but we're not looking at the block ({hit_result:?}.block_pos != {:?}). You should've looked at the block before trying to mine with force=false.",
200                    event.position
201                );
202            };
203        }
204    }
205}
206
207/// Present on entities when they're going to start mining a block next tick.
208#[derive(Clone, Component, Debug)]
209pub struct MiningQueued {
210    pub position: BlockPos,
211    pub direction: Direction,
212    /// Whether we should mine the block regardless of whether it's reachable.
213    pub force: bool,
214}
215#[allow(clippy::too_many_arguments, clippy::type_complexity)]
216pub fn handle_mining_queued(
217    mut commands: Commands,
218    mut attack_block_events: MessageWriter<AttackBlockEvent>,
219    mut mine_block_progress_events: MessageWriter<MineBlockProgressEvent>,
220    query: Query<(
221        Entity,
222        &MiningQueued,
223        &WorldHolder,
224        &GameMode,
225        &Inventory,
226        &ActiveEffects,
227        &FluidOnEyes,
228        &Physics,
229        &Attributes,
230        Option<&mut Mining>,
231        &mut BlockStatePredictionHandler,
232        (
233            &mut MineDelay,
234            &mut MineProgress,
235            &mut MineTicks,
236            &mut MineItem,
237            &mut MineBlockPos,
238        ),
239    )>,
240) {
241    for (
242        entity,
243        mining_queued,
244        world_holder,
245        &game_mode,
246        inventory,
247        active_effects,
248        fluid_on_eyes,
249        physics,
250        attributes,
251        mut mining,
252        mut sequence_number,
253        (
254            mut mine_delay,
255            mut mine_progress,
256            mut mine_ticks,
257            mut current_mining_item,
258            mut current_mining_pos,
259        ),
260    ) in query
261    {
262        trace!("handle_mining_queued {mining_queued:?}");
263        commands.entity(entity).remove::<MiningQueued>();
264
265        let world = world_holder.shared.read();
266        if check_is_interaction_restricted(&world, mining_queued.position, &game_mode, inventory) {
267            continue;
268        }
269        // TODO (when world border is implemented): vanilla ignores if the block
270        // is outside of the worldborder
271
272        if let Some(mining) = &mut mining {
273            // this matters if we were previously mining a block without force
274            if mining_queued.force {
275                mining.force = true;
276            }
277        }
278
279        if game_mode == GameMode::Creative {
280            // In creative mode, first send START_DESTROY_BLOCK packet then immediately
281            // finish mining
282            commands.trigger(SendGamePacketEvent::new(
283                entity,
284                ServerboundPlayerAction {
285                    action: s_player_action::Action::StartDestroyBlock,
286                    pos: mining_queued.position,
287                    direction: mining_queued.direction,
288                    seq: sequence_number.start_predicting(),
289                },
290            ));
291            commands.trigger(FinishMiningBlockEvent {
292                entity,
293                position: mining_queued.position,
294            });
295            **mine_delay = 5;
296            commands.trigger(SwingArmEvent { entity });
297        } else if mining.is_none()
298            || !is_same_mining_target(
299                mining_queued.position,
300                inventory,
301                &current_mining_pos,
302                &current_mining_item,
303            )
304        {
305            if mining.is_some() {
306                // send a packet to stop mining since we just changed target
307                commands.trigger(SendGamePacketEvent::new(
308                    entity,
309                    ServerboundPlayerAction {
310                        action: s_player_action::Action::AbortDestroyBlock,
311                        pos: current_mining_pos
312                            .expect("IsMining is true so MineBlockPos must be present"),
313                        direction: mining_queued.direction,
314                        seq: 0,
315                    },
316                ));
317            }
318
319            let target_block_state = world
320                .get_block_state(mining_queued.position)
321                .unwrap_or_default();
322
323            // we can't break blocks if they don't have a bounding box
324            let block_is_solid = !target_block_state
325                .outline_shape(mining_queued.position)
326                .is_empty();
327
328            if block_is_solid && **mine_progress == 0. {
329                // interact with the block (like note block left click) here
330                attack_block_events.write(AttackBlockEvent {
331                    entity,
332                    position: mining_queued.position,
333                });
334            }
335
336            let held_item = inventory.held_item();
337
338            if block_is_solid
339                && get_mine_progress(
340                    &*target_block_state,
341                    held_item,
342                    fluid_on_eyes,
343                    physics,
344                    attributes,
345                    active_effects,
346                ) >= 1.
347            {
348                // block was broken instantly (instamined)
349                commands.trigger(FinishMiningBlockEvent {
350                    entity,
351                    position: mining_queued.position,
352                });
353            } else {
354                let mining = Mining {
355                    pos: mining_queued.position,
356                    dir: mining_queued.direction,
357                    force: mining_queued.force,
358                };
359                trace!("inserting mining component {mining:?} for entity {entity:?}");
360                commands.entity(entity).insert(mining);
361                **current_mining_pos = Some(mining_queued.position);
362                **current_mining_item = held_item.clone();
363                **mine_progress = 0.;
364                **mine_ticks = 0.;
365                mine_block_progress_events.write(MineBlockProgressEvent {
366                    entity,
367                    position: mining_queued.position,
368                    destroy_stage: mine_progress.destroy_stage(),
369                });
370            }
371
372            commands.trigger(SendGamePacketEvent::new(
373                entity,
374                ServerboundPlayerAction {
375                    action: s_player_action::Action::StartDestroyBlock,
376                    pos: mining_queued.position,
377                    direction: mining_queued.direction,
378                    seq: sequence_number.start_predicting(),
379                },
380            ));
381            commands.trigger(SwingArmEvent { entity });
382            // another swing packet gets sent in the same tick in
383            // continue_mining_block, vanilla does this too
384        }
385    }
386}
387
388#[derive(Message)]
389pub struct MineBlockProgressEvent {
390    pub entity: Entity,
391    pub position: BlockPos,
392    pub destroy_stage: Option<u32>,
393}
394
395/// A player left clicked on a block, used for stuff like interacting with note
396/// blocks.
397#[derive(Message)]
398pub struct AttackBlockEvent {
399    pub entity: Entity,
400    pub position: BlockPos,
401}
402
403/// Returns whether the block and item are still the same as when we started
404/// mining.
405fn is_same_mining_target(
406    target_block: BlockPos,
407    inventory: &Inventory,
408    current_mining_pos: &MineBlockPos,
409    current_mining_item: &MineItem,
410) -> bool {
411    let held_item = inventory.held_item();
412    Some(target_block) == current_mining_pos.0 && held_item == &current_mining_item.0
413}
414
415/// A component bundle for players that can mine blocks.
416#[derive(Bundle, Clone, Default)]
417pub struct MineBundle {
418    pub delay: MineDelay,
419    pub progress: MineProgress,
420    pub ticks: MineTicks,
421    pub mining_pos: MineBlockPos,
422    pub mine_item: MineItem,
423}
424
425/// A component that counts down until we start mining the next block.
426#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
427pub struct MineDelay(pub u32);
428
429/// A component that stores the progress of the current mining operation.
430///
431/// This is a value between 0 and 1.
432#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
433pub struct MineProgress(pub f32);
434
435impl MineProgress {
436    pub fn destroy_stage(&self) -> Option<u32> {
437        if self.0 > 0. {
438            Some((self.0 * 10.) as u32)
439        } else {
440            None
441        }
442    }
443}
444
445/// A component that stores the number of ticks that we've been mining the same
446/// block for.
447///
448/// This is a float despite the fact that it should only ever be a round number.
449#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
450pub struct MineTicks(pub f32);
451
452/// A component that stores the position of the block we're currently mining.
453#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
454pub struct MineBlockPos(pub Option<BlockPos>);
455
456/// A component that contains the item we're currently using to mine, or
457/// [`ItemStack::Empty`] if nothing is being mined.
458#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
459pub struct MineItem(pub ItemStack);
460
461/// A trigger that's sent when we completed mining a block.
462#[derive(EntityEvent)]
463pub struct FinishMiningBlockEvent {
464    pub entity: Entity,
465    pub position: BlockPos,
466}
467
468pub fn handle_finish_mining_block_observer(
469    finish_mining_block: On<FinishMiningBlockEvent>,
470    mut query: Query<(
471        &WorldName,
472        &GameMode,
473        &Inventory,
474        &PlayerAbilities,
475        &PermissionLevel,
476        &Position,
477        &mut BlockStatePredictionHandler,
478    )>,
479    worlds: Res<Worlds>,
480) {
481    let event = finish_mining_block.event();
482
483    let (
484        world_name,
485        &game_mode,
486        inventory,
487        abilities,
488        permission_level,
489        player_pos,
490        mut prediction_handler,
491    ) = query.get_mut(finish_mining_block.entity).unwrap();
492    let world_lock = worlds.get(world_name).unwrap();
493    let world = world_lock.read();
494    if check_is_interaction_restricted(&world, event.position, &game_mode, inventory) {
495        return;
496    }
497
498    if game_mode == GameMode::Creative {
499        let held_item = inventory.held_item().kind();
500        if matches!(held_item, ItemKind::Trident | ItemKind::DebugStick)
501            || azalea_registry::tags::items::SWORDS.contains(&held_item)
502        {
503            return;
504        }
505    }
506
507    let Some(block_state) = world.get_block_state(event.position) else {
508        return;
509    };
510
511    let registry_block = block_state.as_block_kind();
512    if !can_use_game_master_blocks(abilities, permission_level)
513        && matches!(
514            registry_block,
515            BlockKind::CommandBlock | BlockKind::StructureBlock
516        )
517    {
518        return;
519    }
520    if block_state == BlockState::AIR {
521        return;
522    }
523
524    // when we break a waterlogged block we want to keep the water there
525    let fluid_state = FluidState::from(block_state);
526    let block_state_for_fluid = BlockState::from(fluid_state);
527    let old_state = world
528        .set_block_state(event.position, block_state_for_fluid)
529        .unwrap_or_default();
530    prediction_handler.retain_known_server_state(event.position, old_state, **player_pos);
531}
532
533/// Abort mining a block.
534#[derive(Message)]
535pub struct StopMiningBlockEvent {
536    pub entity: Entity,
537}
538pub fn handle_stop_mining_block_event(
539    mut events: MessageReader<StopMiningBlockEvent>,
540    mut commands: Commands,
541    mut mine_block_progress_events: MessageWriter<MineBlockProgressEvent>,
542    mut query: Query<(&MineBlockPos, &mut MineProgress)>,
543) {
544    for event in events.read() {
545        let (mine_block_pos, mut mine_progress) = query.get_mut(event.entity).unwrap();
546
547        let mine_block_pos =
548            mine_block_pos.expect("IsMining is true so MineBlockPos must be present");
549        commands.trigger(SendGamePacketEvent::new(
550            event.entity,
551            ServerboundPlayerAction {
552                action: s_player_action::Action::AbortDestroyBlock,
553                pos: mine_block_pos,
554                direction: Direction::Down,
555                seq: 0,
556            },
557        ));
558        commands.entity(event.entity).remove::<Mining>();
559        **mine_progress = 0.;
560        mine_block_progress_events.write(MineBlockProgressEvent {
561            entity: event.entity,
562            position: mine_block_pos,
563            destroy_stage: None,
564        });
565    }
566}
567
568pub fn decrement_mine_delay(mut query: Query<&mut MineDelay>) {
569    for mut mine_delay in &mut query {
570        if **mine_delay > 0 {
571            **mine_delay -= 1;
572        }
573    }
574}
575
576#[allow(clippy::too_many_arguments, clippy::type_complexity)]
577pub fn continue_mining_block(
578    mut query: Query<(
579        Entity,
580        &WorldName,
581        &GameMode,
582        &Inventory,
583        &MineBlockPos,
584        &MineItem,
585        &ActiveEffects,
586        &FluidOnEyes,
587        &Physics,
588        &Attributes,
589        &Mining,
590        &mut MineDelay,
591        &mut MineProgress,
592        &mut MineTicks,
593        &mut BlockStatePredictionHandler,
594    )>,
595    mut commands: Commands,
596    mut mine_block_progress_events: MessageWriter<MineBlockProgressEvent>,
597    worlds: Res<Worlds>,
598) {
599    for (
600        entity,
601        world_name,
602        &game_mode,
603        inventory,
604        current_mining_pos,
605        current_mining_item,
606        active_effects,
607        fluid_on_eyes,
608        physics,
609        attributes,
610        mining,
611        mut mine_delay,
612        mut mine_progress,
613        mut mine_ticks,
614        mut prediction_handler,
615    ) in query.iter_mut()
616    {
617        if game_mode == GameMode::Creative {
618            // TODO: worldborder check
619            **mine_delay = 5;
620            commands.trigger(SendGamePacketEvent::new(
621                entity,
622                ServerboundPlayerAction {
623                    action: s_player_action::Action::StartDestroyBlock,
624                    pos: mining.pos,
625                    direction: mining.dir,
626                    seq: prediction_handler.start_predicting(),
627                },
628            ));
629            commands.trigger(FinishMiningBlockEvent {
630                entity,
631                position: mining.pos,
632            });
633            commands.trigger(SwingArmEvent { entity });
634        } else if mining.force
635            || is_same_mining_target(
636                mining.pos,
637                inventory,
638                current_mining_pos,
639                current_mining_item,
640            )
641        {
642            trace!("continue mining block at {:?}", mining.pos);
643            let world_lock = worlds.get(world_name).unwrap();
644            let world = world_lock.read();
645            let target_block_state = world.get_block_state(mining.pos).unwrap_or_default();
646
647            trace!("target_block_state: {target_block_state:?}");
648
649            if target_block_state.is_air() {
650                commands.entity(entity).remove::<Mining>();
651                continue;
652            }
653            let block = target_block_state.to_trait();
654            **mine_progress += get_mine_progress(
655                block,
656                current_mining_item,
657                fluid_on_eyes,
658                physics,
659                attributes,
660                active_effects,
661            );
662
663            if **mine_ticks % 4. == 0. {
664                // vanilla makes a mining sound here
665            }
666            **mine_ticks += 1.;
667
668            if **mine_progress >= 1. {
669                // MiningQueued is removed in case we were doing an infinite loop that
670                // repeatedly inserts MiningQueued
671                commands.entity(entity).remove::<(Mining, MiningQueued)>();
672                trace!("finished mining block at {:?}", mining.pos);
673                commands.trigger(FinishMiningBlockEvent {
674                    entity,
675                    position: mining.pos,
676                });
677                commands.trigger(SendGamePacketEvent::new(
678                    entity,
679                    ServerboundPlayerAction {
680                        action: s_player_action::Action::StopDestroyBlock,
681                        pos: mining.pos,
682                        direction: mining.dir,
683                        seq: prediction_handler.start_predicting(),
684                    },
685                ));
686                **mine_progress = 0.;
687                **mine_ticks = 0.;
688                **mine_delay = 5;
689            }
690
691            mine_block_progress_events.write(MineBlockProgressEvent {
692                entity,
693                position: mining.pos,
694                destroy_stage: mine_progress.destroy_stage(),
695            });
696            commands.trigger(SwingArmEvent { entity });
697        } else {
698            trace!("switching mining target to {:?}", mining.pos);
699            commands.entity(entity).insert(MiningQueued {
700                position: mining.pos,
701                direction: mining.dir,
702                force: false,
703            });
704        }
705    }
706}
707
708pub fn update_mining_component(
709    mut commands: Commands,
710    mut query: Query<(Entity, &mut Mining, &HitResultComponent)>,
711) {
712    for (entity, mut mining, hit_result_component) in &mut query.iter_mut() {
713        if let Some(block_hit_result) = hit_result_component.as_block_hit_result_if_not_miss() {
714            if mining.force && block_hit_result.block_pos != mining.pos {
715                continue;
716            }
717
718            if mining.pos != block_hit_result.block_pos {
719                debug!(
720                    "Updating Mining::pos from {:?} to {:?}",
721                    mining.pos, block_hit_result.block_pos
722                );
723                mining.pos = block_hit_result.block_pos;
724            }
725            mining.dir = block_hit_result.direction;
726        } else {
727            if mining.force {
728                continue;
729            }
730
731            debug!("Removing mining component because we're no longer looking at the block");
732            commands.entity(entity).remove::<Mining>();
733        }
734    }
735}