1use azalea_block::{BlockState, BlockTrait, 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::{InstanceContainer, InstanceName};
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::{InstanceHolder, LocalGameMode, PermissionLevel},
24 movement::MoveEventsSystems,
25 packet::game::SendGamePacketEvent,
26};
27
28pub 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#[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)]
71pub struct MiningSystems;
72
73#[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 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#[derive(Clone, Component, Debug)]
133pub struct Mining {
134 pub pos: BlockPos,
135 pub dir: Direction,
136 pub force: bool,
138}
139
140#[derive(Debug, Message)]
145pub struct StartMiningBlockEvent {
146 pub entity: Entity,
147 pub position: BlockPos,
148 pub force: bool,
156}
157fn handle_start_mining_block_event(
158 mut commands: Commands,
159 mut events: MessageReader<StartMiningBlockEvent>,
160 mut query: Query<&HitResultComponent>,
161) {
162 for event in events.read() {
163 trace!("{event:?}");
164 let hit_result = query.get_mut(event.entity).unwrap();
165 if event.force {
166 let direction = if let Some(block_hit_result) =
167 hit_result.as_block_hit_result_if_not_miss()
168 && block_hit_result.block_pos == event.position
169 {
170 block_hit_result.direction
172 } else {
173 debug!(
174 "Got StartMiningBlockEvent but we're not looking at the block ({hit_result:?}.block_pos != {:?}). Picking an arbitrary direction instead.",
175 event.position
176 );
177 Direction::Down
179 };
180 commands.entity(event.entity).insert(MiningQueued {
181 position: event.position,
182 direction,
183 force: true,
184 });
185 } else {
186 if let Some(block_hit_result) = hit_result.as_block_hit_result_if_not_miss()
189 && block_hit_result.block_pos == event.position
190 {
191 commands.entity(event.entity).insert(MiningQueued {
192 position: event.position,
193 direction: block_hit_result.direction,
194 force: false,
195 });
196 } else {
197 warn!(
198 "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.",
199 event.position
200 );
201 };
202 }
203 }
204}
205
206#[derive(Clone, Component, Debug)]
208pub struct MiningQueued {
209 pub position: BlockPos,
210 pub direction: Direction,
211 pub force: bool,
213}
214#[allow(clippy::too_many_arguments, clippy::type_complexity)]
215pub fn handle_mining_queued(
216 mut commands: Commands,
217 mut attack_block_events: MessageWriter<AttackBlockEvent>,
218 mut mine_block_progress_events: MessageWriter<MineBlockProgressEvent>,
219 query: Query<(
220 Entity,
221 &MiningQueued,
222 &InstanceHolder,
223 &LocalGameMode,
224 &Inventory,
225 &ActiveEffects,
226 &FluidOnEyes,
227 &Physics,
228 &Attributes,
229 Option<&mut Mining>,
230 &mut BlockStatePredictionHandler,
231 (
232 &mut MineDelay,
233 &mut MineProgress,
234 &mut MineTicks,
235 &mut MineItem,
236 &mut MineBlockPos,
237 ),
238 )>,
239) {
240 for (
241 entity,
242 mining_queued,
243 instance_holder,
244 game_mode,
245 inventory,
246 active_effects,
247 fluid_on_eyes,
248 physics,
249 attributes,
250 mut mining,
251 mut sequence_number,
252 (
253 mut mine_delay,
254 mut mine_progress,
255 mut mine_ticks,
256 mut current_mining_item,
257 mut current_mining_pos,
258 ),
259 ) in query
260 {
261 trace!("handle_mining_queued {mining_queued:?}");
262 commands.entity(entity).remove::<MiningQueued>();
263
264 let instance = instance_holder.instance.read();
265 if check_is_interaction_restricted(
266 &instance,
267 mining_queued.position,
268 &game_mode.current,
269 inventory,
270 ) {
271 continue;
272 }
273 if let Some(mining) = &mut mining {
277 if mining_queued.force {
279 mining.force = true;
280 }
281 }
282
283 if game_mode.current == GameMode::Creative {
284 commands.trigger(SendGamePacketEvent::new(
287 entity,
288 ServerboundPlayerAction {
289 action: s_player_action::Action::StartDestroyBlock,
290 pos: mining_queued.position,
291 direction: mining_queued.direction,
292 seq: sequence_number.start_predicting(),
293 },
294 ));
295 commands.trigger(FinishMiningBlockEvent {
296 entity,
297 position: mining_queued.position,
298 });
299 **mine_delay = 5;
300 commands.trigger(SwingArmEvent { entity });
301 } else if mining.is_none()
302 || !is_same_mining_target(
303 mining_queued.position,
304 inventory,
305 ¤t_mining_pos,
306 ¤t_mining_item,
307 )
308 {
309 if mining.is_some() {
310 commands.trigger(SendGamePacketEvent::new(
312 entity,
313 ServerboundPlayerAction {
314 action: s_player_action::Action::AbortDestroyBlock,
315 pos: current_mining_pos
316 .expect("IsMining is true so MineBlockPos must be present"),
317 direction: mining_queued.direction,
318 seq: 0,
319 },
320 ));
321 }
322
323 let target_block_state = instance
324 .get_block_state(mining_queued.position)
325 .unwrap_or_default();
326
327 let block_is_solid = !target_block_state.outline_shape().is_empty();
329
330 if block_is_solid && **mine_progress == 0. {
331 attack_block_events.write(AttackBlockEvent {
333 entity,
334 position: mining_queued.position,
335 });
336 }
337
338 let block = Box::<dyn BlockTrait>::from(target_block_state);
339
340 let held_item = inventory.held_item();
341
342 if block_is_solid
343 && get_mine_progress(
344 block.as_ref(),
345 held_item.kind(),
346 fluid_on_eyes,
347 physics,
348 attributes,
349 active_effects,
350 ) >= 1.
351 {
352 commands.trigger(FinishMiningBlockEvent {
354 entity,
355 position: mining_queued.position,
356 });
357 } else {
358 let mining = Mining {
359 pos: mining_queued.position,
360 dir: mining_queued.direction,
361 force: mining_queued.force,
362 };
363 trace!("inserting mining component {mining:?} for entity {entity:?}");
364 commands.entity(entity).insert(mining);
365 **current_mining_pos = Some(mining_queued.position);
366 **current_mining_item = held_item.clone();
367 **mine_progress = 0.;
368 **mine_ticks = 0.;
369 mine_block_progress_events.write(MineBlockProgressEvent {
370 entity,
371 position: mining_queued.position,
372 destroy_stage: mine_progress.destroy_stage(),
373 });
374 }
375
376 commands.trigger(SendGamePacketEvent::new(
377 entity,
378 ServerboundPlayerAction {
379 action: s_player_action::Action::StartDestroyBlock,
380 pos: mining_queued.position,
381 direction: mining_queued.direction,
382 seq: sequence_number.start_predicting(),
383 },
384 ));
385 commands.trigger(SwingArmEvent { entity });
386 }
389 }
390}
391
392#[derive(Message)]
393pub struct MineBlockProgressEvent {
394 pub entity: Entity,
395 pub position: BlockPos,
396 pub destroy_stage: Option<u32>,
397}
398
399#[derive(Message)]
402pub struct AttackBlockEvent {
403 pub entity: Entity,
404 pub position: BlockPos,
405}
406
407fn is_same_mining_target(
410 target_block: BlockPos,
411 inventory: &Inventory,
412 current_mining_pos: &MineBlockPos,
413 current_mining_item: &MineItem,
414) -> bool {
415 let held_item = inventory.held_item();
416 Some(target_block) == current_mining_pos.0 && held_item == ¤t_mining_item.0
417}
418
419#[derive(Bundle, Clone, Default)]
421pub struct MineBundle {
422 pub delay: MineDelay,
423 pub progress: MineProgress,
424 pub ticks: MineTicks,
425 pub mining_pos: MineBlockPos,
426 pub mine_item: MineItem,
427}
428
429#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
431pub struct MineDelay(pub u32);
432
433#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
437pub struct MineProgress(pub f32);
438
439impl MineProgress {
440 pub fn destroy_stage(&self) -> Option<u32> {
441 if self.0 > 0. {
442 Some((self.0 * 10.) as u32)
443 } else {
444 None
445 }
446 }
447}
448
449#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
454pub struct MineTicks(pub f32);
455
456#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
458pub struct MineBlockPos(pub Option<BlockPos>);
459
460#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
463pub struct MineItem(pub ItemStack);
464
465#[derive(EntityEvent)]
467pub struct FinishMiningBlockEvent {
468 pub entity: Entity,
469 pub position: BlockPos,
470}
471
472pub fn handle_finish_mining_block_observer(
473 finish_mining_block: On<FinishMiningBlockEvent>,
474 mut query: Query<(
475 &InstanceName,
476 &LocalGameMode,
477 &Inventory,
478 &PlayerAbilities,
479 &PermissionLevel,
480 &Position,
481 &mut BlockStatePredictionHandler,
482 )>,
483 instances: Res<InstanceContainer>,
484) {
485 let event = finish_mining_block.event();
486
487 let (
488 instance_name,
489 game_mode,
490 inventory,
491 abilities,
492 permission_level,
493 player_pos,
494 mut prediction_handler,
495 ) = query.get_mut(finish_mining_block.entity).unwrap();
496 let instance_lock = instances.get(instance_name).unwrap();
497 let instance = instance_lock.read();
498 if check_is_interaction_restricted(&instance, event.position, &game_mode.current, inventory) {
499 return;
500 }
501
502 if game_mode.current == GameMode::Creative {
503 let held_item = inventory.held_item().kind();
504 if matches!(held_item, ItemKind::Trident | ItemKind::DebugStick)
505 || azalea_registry::tags::items::SWORDS.contains(&held_item)
506 {
507 return;
508 }
509 }
510
511 let Some(block_state) = instance.get_block_state(event.position) else {
512 return;
513 };
514
515 let registry_block = Box::<dyn BlockTrait>::from(block_state).as_registry_block();
516 if !can_use_game_master_blocks(abilities, permission_level)
517 && matches!(
518 registry_block,
519 BlockKind::CommandBlock | BlockKind::StructureBlock
520 )
521 {
522 return;
523 }
524 if block_state == BlockState::AIR {
525 return;
526 }
527
528 let fluid_state = FluidState::from(block_state);
530 let block_state_for_fluid = BlockState::from(fluid_state);
531 let old_state = instance
532 .set_block_state(event.position, block_state_for_fluid)
533 .unwrap_or_default();
534 prediction_handler.retain_known_server_state(event.position, old_state, **player_pos);
535}
536
537#[derive(Message)]
539pub struct StopMiningBlockEvent {
540 pub entity: Entity,
541}
542pub fn handle_stop_mining_block_event(
543 mut events: MessageReader<StopMiningBlockEvent>,
544 mut commands: Commands,
545 mut mine_block_progress_events: MessageWriter<MineBlockProgressEvent>,
546 mut query: Query<(&MineBlockPos, &mut MineProgress)>,
547) {
548 for event in events.read() {
549 let (mine_block_pos, mut mine_progress) = query.get_mut(event.entity).unwrap();
550
551 let mine_block_pos =
552 mine_block_pos.expect("IsMining is true so MineBlockPos must be present");
553 commands.trigger(SendGamePacketEvent::new(
554 event.entity,
555 ServerboundPlayerAction {
556 action: s_player_action::Action::AbortDestroyBlock,
557 pos: mine_block_pos,
558 direction: Direction::Down,
559 seq: 0,
560 },
561 ));
562 commands.entity(event.entity).remove::<Mining>();
563 **mine_progress = 0.;
564 mine_block_progress_events.write(MineBlockProgressEvent {
565 entity: event.entity,
566 position: mine_block_pos,
567 destroy_stage: None,
568 });
569 }
570}
571
572pub fn decrement_mine_delay(mut query: Query<&mut MineDelay>) {
573 for mut mine_delay in &mut query {
574 if **mine_delay > 0 {
575 **mine_delay -= 1;
576 }
577 }
578}
579
580#[allow(clippy::too_many_arguments, clippy::type_complexity)]
581pub fn continue_mining_block(
582 mut query: Query<(
583 Entity,
584 &InstanceName,
585 &LocalGameMode,
586 &Inventory,
587 &MineBlockPos,
588 &MineItem,
589 &ActiveEffects,
590 &FluidOnEyes,
591 &Physics,
592 &Attributes,
593 &Mining,
594 &mut MineDelay,
595 &mut MineProgress,
596 &mut MineTicks,
597 &mut BlockStatePredictionHandler,
598 )>,
599 mut commands: Commands,
600 mut mine_block_progress_events: MessageWriter<MineBlockProgressEvent>,
601 instances: Res<InstanceContainer>,
602) {
603 for (
604 entity,
605 instance_name,
606 game_mode,
607 inventory,
608 current_mining_pos,
609 current_mining_item,
610 active_effects,
611 fluid_on_eyes,
612 physics,
613 attributes,
614 mining,
615 mut mine_delay,
616 mut mine_progress,
617 mut mine_ticks,
618 mut prediction_handler,
619 ) in query.iter_mut()
620 {
621 if game_mode.current == GameMode::Creative {
622 **mine_delay = 5;
624 commands.trigger(SendGamePacketEvent::new(
625 entity,
626 ServerboundPlayerAction {
627 action: s_player_action::Action::StartDestroyBlock,
628 pos: mining.pos,
629 direction: mining.dir,
630 seq: prediction_handler.start_predicting(),
631 },
632 ));
633 commands.trigger(FinishMiningBlockEvent {
634 entity,
635 position: mining.pos,
636 });
637 commands.trigger(SwingArmEvent { entity });
638 } else if mining.force
639 || is_same_mining_target(
640 mining.pos,
641 inventory,
642 current_mining_pos,
643 current_mining_item,
644 )
645 {
646 trace!("continue mining block at {:?}", mining.pos);
647 let instance_lock = instances.get(instance_name).unwrap();
648 let instance = instance_lock.read();
649 let target_block_state = instance.get_block_state(mining.pos).unwrap_or_default();
650
651 trace!("target_block_state: {target_block_state:?}");
652
653 if target_block_state.is_air() {
654 commands.entity(entity).remove::<Mining>();
655 continue;
656 }
657 let block = Box::<dyn BlockTrait>::from(target_block_state);
658 **mine_progress += get_mine_progress(
659 block.as_ref(),
660 current_mining_item.kind(),
661 fluid_on_eyes,
662 physics,
663 attributes,
664 active_effects,
665 );
666
667 if **mine_ticks % 4. == 0. {
668 }
670 **mine_ticks += 1.;
671
672 if **mine_progress >= 1. {
673 commands.entity(entity).remove::<(Mining, MiningQueued)>();
676 trace!("finished mining block at {:?}", mining.pos);
677 commands.trigger(FinishMiningBlockEvent {
678 entity,
679 position: mining.pos,
680 });
681 commands.trigger(SendGamePacketEvent::new(
682 entity,
683 ServerboundPlayerAction {
684 action: s_player_action::Action::StopDestroyBlock,
685 pos: mining.pos,
686 direction: mining.dir,
687 seq: prediction_handler.start_predicting(),
688 },
689 ));
690 **mine_progress = 0.;
691 **mine_ticks = 0.;
692 **mine_delay = 5;
693 }
694
695 mine_block_progress_events.write(MineBlockProgressEvent {
696 entity,
697 position: mining.pos,
698 destroy_stage: mine_progress.destroy_stage(),
699 });
700 commands.trigger(SwingArmEvent { entity });
701 } else {
702 trace!("switching mining target to {:?}", mining.pos);
703 commands.entity(entity).insert(MiningQueued {
704 position: mining.pos,
705 direction: mining.dir,
706 force: false,
707 });
708 }
709 }
710}
711
712pub fn update_mining_component(
713 mut commands: Commands,
714 mut query: Query<(Entity, &mut Mining, &HitResultComponent)>,
715) {
716 for (entity, mut mining, hit_result_component) in &mut query.iter_mut() {
717 if let Some(block_hit_result) = hit_result_component.as_block_hit_result_if_not_miss() {
718 if mining.force && block_hit_result.block_pos != mining.pos {
719 continue;
720 }
721
722 if mining.pos != block_hit_result.block_pos {
723 debug!(
724 "Updating Mining::pos from {:?} to {:?}",
725 mining.pos, block_hit_result.block_pos
726 );
727 mining.pos = block_hit_result.block_pos;
728 }
729 mining.dir = block_hit_result.direction;
730 } else {
731 if mining.force {
732 continue;
733 }
734
735 debug!("Removing mining component because we're no longer looking at the block");
736 commands.entity(entity).remove::<Mining>();
737 }
738 }
739}