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::{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::{LocalGameMode, PermissionLevel, WorldHolder},
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)]
133#[component(storage = "SparseSet")]
134pub struct Mining {
135 pub pos: BlockPos,
136 pub dir: Direction,
137 pub force: bool,
139}
140
141#[derive(Debug, Message)]
146pub struct StartMiningBlockEvent {
147 pub entity: Entity,
148 pub position: BlockPos,
149 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 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 Direction::Down
180 };
181 commands.entity(event.entity).insert(MiningQueued {
182 position: event.position,
183 direction,
184 force: true,
185 });
186 } else {
187 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#[derive(Clone, Component, Debug)]
209pub struct MiningQueued {
210 pub position: BlockPos,
211 pub direction: Direction,
212 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 &LocalGameMode,
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(
267 &world,
268 mining_queued.position,
269 &game_mode.current,
270 inventory,
271 ) {
272 continue;
273 }
274 if let Some(mining) = &mut mining {
278 if mining_queued.force {
280 mining.force = true;
281 }
282 }
283
284 if game_mode.current == GameMode::Creative {
285 commands.trigger(SendGamePacketEvent::new(
288 entity,
289 ServerboundPlayerAction {
290 action: s_player_action::Action::StartDestroyBlock,
291 pos: mining_queued.position,
292 direction: mining_queued.direction,
293 seq: sequence_number.start_predicting(),
294 },
295 ));
296 commands.trigger(FinishMiningBlockEvent {
297 entity,
298 position: mining_queued.position,
299 });
300 **mine_delay = 5;
301 commands.trigger(SwingArmEvent { entity });
302 } else if mining.is_none()
303 || !is_same_mining_target(
304 mining_queued.position,
305 inventory,
306 ¤t_mining_pos,
307 ¤t_mining_item,
308 )
309 {
310 if mining.is_some() {
311 commands.trigger(SendGamePacketEvent::new(
313 entity,
314 ServerboundPlayerAction {
315 action: s_player_action::Action::AbortDestroyBlock,
316 pos: current_mining_pos
317 .expect("IsMining is true so MineBlockPos must be present"),
318 direction: mining_queued.direction,
319 seq: 0,
320 },
321 ));
322 }
323
324 let target_block_state = world
325 .get_block_state(mining_queued.position)
326 .unwrap_or_default();
327
328 let block_is_solid = !target_block_state
330 .outline_shape(mining_queued.position)
331 .is_empty();
332
333 if block_is_solid && **mine_progress == 0. {
334 attack_block_events.write(AttackBlockEvent {
336 entity,
337 position: mining_queued.position,
338 });
339 }
340
341 let block = Box::<dyn BlockTrait>::from(target_block_state);
342
343 let held_item = inventory.held_item();
344
345 if block_is_solid
346 && get_mine_progress(
347 block.as_ref(),
348 held_item,
349 fluid_on_eyes,
350 physics,
351 attributes,
352 active_effects,
353 ) >= 1.
354 {
355 commands.trigger(FinishMiningBlockEvent {
357 entity,
358 position: mining_queued.position,
359 });
360 } else {
361 let mining = Mining {
362 pos: mining_queued.position,
363 dir: mining_queued.direction,
364 force: mining_queued.force,
365 };
366 trace!("inserting mining component {mining:?} for entity {entity:?}");
367 commands.entity(entity).insert(mining);
368 **current_mining_pos = Some(mining_queued.position);
369 **current_mining_item = held_item.clone();
370 **mine_progress = 0.;
371 **mine_ticks = 0.;
372 mine_block_progress_events.write(MineBlockProgressEvent {
373 entity,
374 position: mining_queued.position,
375 destroy_stage: mine_progress.destroy_stage(),
376 });
377 }
378
379 commands.trigger(SendGamePacketEvent::new(
380 entity,
381 ServerboundPlayerAction {
382 action: s_player_action::Action::StartDestroyBlock,
383 pos: mining_queued.position,
384 direction: mining_queued.direction,
385 seq: sequence_number.start_predicting(),
386 },
387 ));
388 commands.trigger(SwingArmEvent { entity });
389 }
392 }
393}
394
395#[derive(Message)]
396pub struct MineBlockProgressEvent {
397 pub entity: Entity,
398 pub position: BlockPos,
399 pub destroy_stage: Option<u32>,
400}
401
402#[derive(Message)]
405pub struct AttackBlockEvent {
406 pub entity: Entity,
407 pub position: BlockPos,
408}
409
410fn is_same_mining_target(
413 target_block: BlockPos,
414 inventory: &Inventory,
415 current_mining_pos: &MineBlockPos,
416 current_mining_item: &MineItem,
417) -> bool {
418 let held_item = inventory.held_item();
419 Some(target_block) == current_mining_pos.0 && held_item == ¤t_mining_item.0
420}
421
422#[derive(Bundle, Clone, Default)]
424pub struct MineBundle {
425 pub delay: MineDelay,
426 pub progress: MineProgress,
427 pub ticks: MineTicks,
428 pub mining_pos: MineBlockPos,
429 pub mine_item: MineItem,
430}
431
432#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
434pub struct MineDelay(pub u32);
435
436#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
440pub struct MineProgress(pub f32);
441
442impl MineProgress {
443 pub fn destroy_stage(&self) -> Option<u32> {
444 if self.0 > 0. {
445 Some((self.0 * 10.) as u32)
446 } else {
447 None
448 }
449 }
450}
451
452#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
457pub struct MineTicks(pub f32);
458
459#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
461pub struct MineBlockPos(pub Option<BlockPos>);
462
463#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
466pub struct MineItem(pub ItemStack);
467
468#[derive(EntityEvent)]
470pub struct FinishMiningBlockEvent {
471 pub entity: Entity,
472 pub position: BlockPos,
473}
474
475pub fn handle_finish_mining_block_observer(
476 finish_mining_block: On<FinishMiningBlockEvent>,
477 mut query: Query<(
478 &WorldName,
479 &LocalGameMode,
480 &Inventory,
481 &PlayerAbilities,
482 &PermissionLevel,
483 &Position,
484 &mut BlockStatePredictionHandler,
485 )>,
486 worlds: Res<Worlds>,
487) {
488 let event = finish_mining_block.event();
489
490 let (
491 world_name,
492 game_mode,
493 inventory,
494 abilities,
495 permission_level,
496 player_pos,
497 mut prediction_handler,
498 ) = query.get_mut(finish_mining_block.entity).unwrap();
499 let world_lock = worlds.get(world_name).unwrap();
500 let world = world_lock.read();
501 if check_is_interaction_restricted(&world, event.position, &game_mode.current, inventory) {
502 return;
503 }
504
505 if game_mode.current == GameMode::Creative {
506 let held_item = inventory.held_item().kind();
507 if matches!(held_item, ItemKind::Trident | ItemKind::DebugStick)
508 || azalea_registry::tags::items::SWORDS.contains(&held_item)
509 {
510 return;
511 }
512 }
513
514 let Some(block_state) = world.get_block_state(event.position) else {
515 return;
516 };
517
518 let registry_block = block_state.as_block_kind();
519 if !can_use_game_master_blocks(abilities, permission_level)
520 && matches!(
521 registry_block,
522 BlockKind::CommandBlock | BlockKind::StructureBlock
523 )
524 {
525 return;
526 }
527 if block_state == BlockState::AIR {
528 return;
529 }
530
531 let fluid_state = FluidState::from(block_state);
533 let block_state_for_fluid = BlockState::from(fluid_state);
534 let old_state = world
535 .set_block_state(event.position, block_state_for_fluid)
536 .unwrap_or_default();
537 prediction_handler.retain_known_server_state(event.position, old_state, **player_pos);
538}
539
540#[derive(Message)]
542pub struct StopMiningBlockEvent {
543 pub entity: Entity,
544}
545pub fn handle_stop_mining_block_event(
546 mut events: MessageReader<StopMiningBlockEvent>,
547 mut commands: Commands,
548 mut mine_block_progress_events: MessageWriter<MineBlockProgressEvent>,
549 mut query: Query<(&MineBlockPos, &mut MineProgress)>,
550) {
551 for event in events.read() {
552 let (mine_block_pos, mut mine_progress) = query.get_mut(event.entity).unwrap();
553
554 let mine_block_pos =
555 mine_block_pos.expect("IsMining is true so MineBlockPos must be present");
556 commands.trigger(SendGamePacketEvent::new(
557 event.entity,
558 ServerboundPlayerAction {
559 action: s_player_action::Action::AbortDestroyBlock,
560 pos: mine_block_pos,
561 direction: Direction::Down,
562 seq: 0,
563 },
564 ));
565 commands.entity(event.entity).remove::<Mining>();
566 **mine_progress = 0.;
567 mine_block_progress_events.write(MineBlockProgressEvent {
568 entity: event.entity,
569 position: mine_block_pos,
570 destroy_stage: None,
571 });
572 }
573}
574
575pub fn decrement_mine_delay(mut query: Query<&mut MineDelay>) {
576 for mut mine_delay in &mut query {
577 if **mine_delay > 0 {
578 **mine_delay -= 1;
579 }
580 }
581}
582
583#[allow(clippy::too_many_arguments, clippy::type_complexity)]
584pub fn continue_mining_block(
585 mut query: Query<(
586 Entity,
587 &WorldName,
588 &LocalGameMode,
589 &Inventory,
590 &MineBlockPos,
591 &MineItem,
592 &ActiveEffects,
593 &FluidOnEyes,
594 &Physics,
595 &Attributes,
596 &Mining,
597 &mut MineDelay,
598 &mut MineProgress,
599 &mut MineTicks,
600 &mut BlockStatePredictionHandler,
601 )>,
602 mut commands: Commands,
603 mut mine_block_progress_events: MessageWriter<MineBlockProgressEvent>,
604 worlds: Res<Worlds>,
605) {
606 for (
607 entity,
608 world_name,
609 game_mode,
610 inventory,
611 current_mining_pos,
612 current_mining_item,
613 active_effects,
614 fluid_on_eyes,
615 physics,
616 attributes,
617 mining,
618 mut mine_delay,
619 mut mine_progress,
620 mut mine_ticks,
621 mut prediction_handler,
622 ) in query.iter_mut()
623 {
624 if game_mode.current == GameMode::Creative {
625 **mine_delay = 5;
627 commands.trigger(SendGamePacketEvent::new(
628 entity,
629 ServerboundPlayerAction {
630 action: s_player_action::Action::StartDestroyBlock,
631 pos: mining.pos,
632 direction: mining.dir,
633 seq: prediction_handler.start_predicting(),
634 },
635 ));
636 commands.trigger(FinishMiningBlockEvent {
637 entity,
638 position: mining.pos,
639 });
640 commands.trigger(SwingArmEvent { entity });
641 } else if mining.force
642 || is_same_mining_target(
643 mining.pos,
644 inventory,
645 current_mining_pos,
646 current_mining_item,
647 )
648 {
649 trace!("continue mining block at {:?}", mining.pos);
650 let world_lock = worlds.get(world_name).unwrap();
651 let world = world_lock.read();
652 let target_block_state = world.get_block_state(mining.pos).unwrap_or_default();
653
654 trace!("target_block_state: {target_block_state:?}");
655
656 if target_block_state.is_air() {
657 commands.entity(entity).remove::<Mining>();
658 continue;
659 }
660 let block = Box::<dyn BlockTrait>::from(target_block_state);
661 **mine_progress += get_mine_progress(
662 block.as_ref(),
663 current_mining_item,
664 fluid_on_eyes,
665 physics,
666 attributes,
667 active_effects,
668 );
669
670 if **mine_ticks % 4. == 0. {
671 }
673 **mine_ticks += 1.;
674
675 if **mine_progress >= 1. {
676 commands.entity(entity).remove::<(Mining, MiningQueued)>();
679 trace!("finished mining block at {:?}", mining.pos);
680 commands.trigger(FinishMiningBlockEvent {
681 entity,
682 position: mining.pos,
683 });
684 commands.trigger(SendGamePacketEvent::new(
685 entity,
686 ServerboundPlayerAction {
687 action: s_player_action::Action::StopDestroyBlock,
688 pos: mining.pos,
689 direction: mining.dir,
690 seq: prediction_handler.start_predicting(),
691 },
692 ));
693 **mine_progress = 0.;
694 **mine_ticks = 0.;
695 **mine_delay = 5;
696 }
697
698 mine_block_progress_events.write(MineBlockProgressEvent {
699 entity,
700 position: mining.pos,
701 destroy_stage: mine_progress.destroy_stage(),
702 });
703 commands.trigger(SwingArmEvent { entity });
704 } else {
705 trace!("switching mining target to {:?}", mining.pos);
706 commands.entity(entity).insert(MiningQueued {
707 position: mining.pos,
708 direction: mining.dir,
709 force: false,
710 });
711 }
712 }
713}
714
715pub fn update_mining_component(
716 mut commands: Commands,
717 mut query: Query<(Entity, &mut Mining, &HitResultComponent)>,
718) {
719 for (entity, mut mining, hit_result_component) in &mut query.iter_mut() {
720 if let Some(block_hit_result) = hit_result_component.as_block_hit_result_if_not_miss() {
721 if mining.force && block_hit_result.block_pos != mining.pos {
722 continue;
723 }
724
725 if mining.pos != block_hit_result.block_pos {
726 debug!(
727 "Updating Mining::pos from {:?} to {:?}",
728 mining.pos, block_hit_result.block_pos
729 );
730 mining.pos = block_hit_result.block_pos;
731 }
732 mining.dir = block_hit_result.direction;
733 } else {
734 if mining.force {
735 continue;
736 }
737
738 debug!("Removing mining component because we're no longer looking at the block");
739 commands.entity(entity).remove::<Mining>();
740 }
741 }
742}