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
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 &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 if let Some(mining) = &mut mining {
273 if mining_queued.force {
275 mining.force = true;
276 }
277 }
278
279 if game_mode == GameMode::Creative {
280 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 ¤t_mining_pos,
302 ¤t_mining_item,
303 )
304 {
305 if mining.is_some() {
306 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 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 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 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 }
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#[derive(Message)]
398pub struct AttackBlockEvent {
399 pub entity: Entity,
400 pub position: BlockPos,
401}
402
403fn 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 == ¤t_mining_item.0
413}
414
415#[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#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
427pub struct MineDelay(pub u32);
428
429#[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#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
450pub struct MineTicks(pub f32);
451
452#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
454pub struct MineBlockPos(pub Option<BlockPos>);
455
456#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
459pub struct MineItem(pub ItemStack);
460
461#[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 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#[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 **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 }
666 **mine_ticks += 1.;
667
668 if **mine_progress >= 1. {
669 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}