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 Client,
19 interact::{
20 BlockStatePredictionHandler, SwingArmEvent, can_use_game_master_blocks,
21 check_is_interaction_restricted, pick::HitResultComponent,
22 },
23 inventory::InventorySystems,
24 local_player::{InstanceHolder, LocalGameMode, PermissionLevel},
25 movement::MoveEventsSystems,
26 packet::game::SendGamePacketEvent,
27};
28
29pub struct MiningPlugin;
31impl Plugin for MiningPlugin {
32 fn build(&self, app: &mut App) {
33 app.add_message::<StartMiningBlockEvent>()
34 .add_message::<StopMiningBlockEvent>()
35 .add_message::<MineBlockProgressEvent>()
36 .add_message::<AttackBlockEvent>()
37 .add_systems(
38 GameTick,
39 (
40 update_mining_component,
41 handle_auto_mine,
42 handle_mining_queued,
43 decrement_mine_delay,
44 continue_mining_block,
45 )
46 .chain()
47 .before(PhysicsSystems)
48 .before(super::movement::send_position)
49 .before(super::interact::handle_start_use_item_queued)
50 .after(azalea_entity::update_fluid_on_eyes)
51 .in_set(MiningSystems),
52 )
53 .add_systems(
54 Update,
55 (
56 handle_start_mining_block_event,
57 handle_stop_mining_block_event,
58 )
59 .chain()
60 .in_set(MiningSystems)
61 .after(InventorySystems)
62 .after(MoveEventsSystems)
63 .after(crate::interact::pick::update_hit_result_component)
64 .after(crate::attack::handle_attack_event),
65 )
66 .add_observer(handle_finish_mining_block_observer);
67 }
68}
69
70#[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)]
72pub struct MiningSystems;
73
74impl Client {
75 pub fn start_mining(&self, position: BlockPos) {
76 let mut ecs = self.ecs.lock();
77
78 ecs.write_message(StartMiningBlockEvent {
79 entity: self.entity,
80 position,
81 force: true,
82 });
83 }
84
85 pub fn left_click_mine(&self, enabled: bool) {
88 let mut ecs = self.ecs.lock();
89 let mut entity_mut = ecs.entity_mut(self.entity);
90
91 if enabled {
92 entity_mut.insert(LeftClickMine);
93 } else {
94 entity_mut.remove::<LeftClickMine>();
95 }
96 }
97}
98
99#[derive(Component)]
103pub struct LeftClickMine;
104
105#[allow(clippy::type_complexity)]
106fn handle_auto_mine(
107 mut query: Query<
108 (
109 &HitResultComponent,
110 Entity,
111 Option<&Mining>,
112 &Inventory,
113 &MineBlockPos,
114 &MineItem,
115 ),
116 With<LeftClickMine>,
117 >,
118 mut start_mining_block_event: MessageWriter<StartMiningBlockEvent>,
119 mut stop_mining_block_event: MessageWriter<StopMiningBlockEvent>,
120) {
121 for (
122 hit_result_component,
123 entity,
124 mining,
125 inventory,
126 current_mining_pos,
127 current_mining_item,
128 ) in &mut query.iter_mut()
129 {
130 let block_pos = hit_result_component
131 .as_block_hit_result_if_not_miss()
132 .map(|b| b.block_pos);
133
134 if let Some(block_pos) = block_pos
136 && (mining.is_none()
137 || !is_same_mining_target(
138 block_pos,
139 inventory,
140 current_mining_pos,
141 current_mining_item,
142 ))
143 {
144 start_mining_block_event.write(StartMiningBlockEvent {
145 entity,
146 position: block_pos,
147 force: true,
148 });
149 } else if mining.is_some() && hit_result_component.miss() {
150 stop_mining_block_event.write(StopMiningBlockEvent { entity });
151 }
152 }
153}
154
155#[derive(Clone, Component, Debug)]
159pub struct Mining {
160 pub pos: BlockPos,
161 pub dir: Direction,
162 pub force: bool,
164}
165
166#[derive(Debug, Message)]
171pub struct StartMiningBlockEvent {
172 pub entity: Entity,
173 pub position: BlockPos,
174 pub force: bool,
182}
183fn handle_start_mining_block_event(
184 mut commands: Commands,
185 mut events: MessageReader<StartMiningBlockEvent>,
186 mut query: Query<&HitResultComponent>,
187) {
188 for event in events.read() {
189 trace!("{event:?}");
190 let hit_result = query.get_mut(event.entity).unwrap();
191 if event.force {
192 let direction = if let Some(block_hit_result) =
193 hit_result.as_block_hit_result_if_not_miss()
194 && block_hit_result.block_pos == event.position
195 {
196 block_hit_result.direction
198 } else {
199 debug!(
200 "Got StartMiningBlockEvent but we're not looking at the block ({hit_result:?}.block_pos != {:?}). Picking an arbitrary direction instead.",
201 event.position
202 );
203 Direction::Down
205 };
206 commands.entity(event.entity).insert(MiningQueued {
207 position: event.position,
208 direction,
209 force: true,
210 });
211 } else {
212 if let Some(block_hit_result) = hit_result.as_block_hit_result_if_not_miss()
215 && block_hit_result.block_pos == event.position
216 {
217 commands.entity(event.entity).insert(MiningQueued {
218 position: event.position,
219 direction: block_hit_result.direction,
220 force: false,
221 });
222 } else {
223 warn!(
224 "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.",
225 event.position
226 );
227 };
228 }
229 }
230}
231
232#[derive(Clone, Component, Debug)]
234pub struct MiningQueued {
235 pub position: BlockPos,
236 pub direction: Direction,
237 pub force: bool,
239}
240#[allow(clippy::too_many_arguments, clippy::type_complexity)]
241pub fn handle_mining_queued(
242 mut commands: Commands,
243 mut attack_block_events: MessageWriter<AttackBlockEvent>,
244 mut mine_block_progress_events: MessageWriter<MineBlockProgressEvent>,
245 query: Query<(
246 Entity,
247 &MiningQueued,
248 &InstanceHolder,
249 &LocalGameMode,
250 &Inventory,
251 &ActiveEffects,
252 &FluidOnEyes,
253 &Physics,
254 &Attributes,
255 Option<&mut Mining>,
256 &mut BlockStatePredictionHandler,
257 (
258 &mut MineDelay,
259 &mut MineProgress,
260 &mut MineTicks,
261 &mut MineItem,
262 &mut MineBlockPos,
263 ),
264 )>,
265) {
266 for (
267 entity,
268 mining_queued,
269 instance_holder,
270 game_mode,
271 inventory,
272 active_effects,
273 fluid_on_eyes,
274 physics,
275 attributes,
276 mut mining,
277 mut sequence_number,
278 (
279 mut mine_delay,
280 mut mine_progress,
281 mut mine_ticks,
282 mut current_mining_item,
283 mut current_mining_pos,
284 ),
285 ) in query
286 {
287 trace!("handle_mining_queued {mining_queued:?}");
288 commands.entity(entity).remove::<MiningQueued>();
289
290 let instance = instance_holder.instance.read();
291 if check_is_interaction_restricted(
292 &instance,
293 mining_queued.position,
294 &game_mode.current,
295 inventory,
296 ) {
297 continue;
298 }
299 if let Some(mining) = &mut mining {
303 if mining_queued.force {
305 mining.force = true;
306 }
307 }
308
309 if game_mode.current == GameMode::Creative {
310 commands.trigger(SendGamePacketEvent::new(
313 entity,
314 ServerboundPlayerAction {
315 action: s_player_action::Action::StartDestroyBlock,
316 pos: mining_queued.position,
317 direction: mining_queued.direction,
318 seq: sequence_number.start_predicting(),
319 },
320 ));
321 commands.trigger(FinishMiningBlockEvent {
322 entity,
323 position: mining_queued.position,
324 });
325 **mine_delay = 5;
326 commands.trigger(SwingArmEvent { entity });
327 } else if mining.is_none()
328 || !is_same_mining_target(
329 mining_queued.position,
330 inventory,
331 ¤t_mining_pos,
332 ¤t_mining_item,
333 )
334 {
335 if mining.is_some() {
336 commands.trigger(SendGamePacketEvent::new(
338 entity,
339 ServerboundPlayerAction {
340 action: s_player_action::Action::AbortDestroyBlock,
341 pos: current_mining_pos
342 .expect("IsMining is true so MineBlockPos must be present"),
343 direction: mining_queued.direction,
344 seq: 0,
345 },
346 ));
347 }
348
349 let target_block_state = instance
350 .get_block_state(mining_queued.position)
351 .unwrap_or_default();
352
353 let block_is_solid = !target_block_state.outline_shape().is_empty();
355
356 if block_is_solid && **mine_progress == 0. {
357 attack_block_events.write(AttackBlockEvent {
359 entity,
360 position: mining_queued.position,
361 });
362 }
363
364 let block = Box::<dyn BlockTrait>::from(target_block_state);
365
366 let held_item = inventory.held_item();
367
368 if block_is_solid
369 && get_mine_progress(
370 block.as_ref(),
371 held_item.kind(),
372 fluid_on_eyes,
373 physics,
374 attributes,
375 active_effects,
376 ) >= 1.
377 {
378 commands.trigger(FinishMiningBlockEvent {
380 entity,
381 position: mining_queued.position,
382 });
383 } else {
384 let mining = Mining {
385 pos: mining_queued.position,
386 dir: mining_queued.direction,
387 force: mining_queued.force,
388 };
389 trace!("inserting mining component {mining:?} for entity {entity:?}");
390 commands.entity(entity).insert(mining);
391 **current_mining_pos = Some(mining_queued.position);
392 **current_mining_item = held_item.clone();
393 **mine_progress = 0.;
394 **mine_ticks = 0.;
395 mine_block_progress_events.write(MineBlockProgressEvent {
396 entity,
397 position: mining_queued.position,
398 destroy_stage: mine_progress.destroy_stage(),
399 });
400 }
401
402 commands.trigger(SendGamePacketEvent::new(
403 entity,
404 ServerboundPlayerAction {
405 action: s_player_action::Action::StartDestroyBlock,
406 pos: mining_queued.position,
407 direction: mining_queued.direction,
408 seq: sequence_number.start_predicting(),
409 },
410 ));
411 commands.trigger(SwingArmEvent { entity });
412 }
415 }
416}
417
418#[derive(Message)]
419pub struct MineBlockProgressEvent {
420 pub entity: Entity,
421 pub position: BlockPos,
422 pub destroy_stage: Option<u32>,
423}
424
425#[derive(Message)]
428pub struct AttackBlockEvent {
429 pub entity: Entity,
430 pub position: BlockPos,
431}
432
433fn is_same_mining_target(
436 target_block: BlockPos,
437 inventory: &Inventory,
438 current_mining_pos: &MineBlockPos,
439 current_mining_item: &MineItem,
440) -> bool {
441 let held_item = inventory.held_item();
442 Some(target_block) == current_mining_pos.0 && held_item == ¤t_mining_item.0
443}
444
445#[derive(Bundle, Clone, Default)]
447pub struct MineBundle {
448 pub delay: MineDelay,
449 pub progress: MineProgress,
450 pub ticks: MineTicks,
451 pub mining_pos: MineBlockPos,
452 pub mine_item: MineItem,
453}
454
455#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
457pub struct MineDelay(pub u32);
458
459#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
463pub struct MineProgress(pub f32);
464
465impl MineProgress {
466 pub fn destroy_stage(&self) -> Option<u32> {
467 if self.0 > 0. {
468 Some((self.0 * 10.) as u32)
469 } else {
470 None
471 }
472 }
473}
474
475#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
480pub struct MineTicks(pub f32);
481
482#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
484pub struct MineBlockPos(pub Option<BlockPos>);
485
486#[derive(Clone, Component, Debug, Default, Deref, DerefMut)]
489pub struct MineItem(pub ItemStack);
490
491#[derive(EntityEvent)]
493pub struct FinishMiningBlockEvent {
494 pub entity: Entity,
495 pub position: BlockPos,
496}
497
498pub fn handle_finish_mining_block_observer(
499 finish_mining_block: On<FinishMiningBlockEvent>,
500 mut query: Query<(
501 &InstanceName,
502 &LocalGameMode,
503 &Inventory,
504 &PlayerAbilities,
505 &PermissionLevel,
506 &Position,
507 &mut BlockStatePredictionHandler,
508 )>,
509 instances: Res<InstanceContainer>,
510) {
511 let event = finish_mining_block.event();
512
513 let (
514 instance_name,
515 game_mode,
516 inventory,
517 abilities,
518 permission_level,
519 player_pos,
520 mut prediction_handler,
521 ) = query.get_mut(finish_mining_block.entity).unwrap();
522 let instance_lock = instances.get(instance_name).unwrap();
523 let instance = instance_lock.read();
524 if check_is_interaction_restricted(&instance, event.position, &game_mode.current, inventory) {
525 return;
526 }
527
528 if game_mode.current == GameMode::Creative {
529 let held_item = inventory.held_item().kind();
530 if matches!(held_item, ItemKind::Trident | ItemKind::DebugStick)
531 || azalea_registry::tags::items::SWORDS.contains(&held_item)
532 {
533 return;
534 }
535 }
536
537 let Some(block_state) = instance.get_block_state(event.position) else {
538 return;
539 };
540
541 let registry_block = Box::<dyn BlockTrait>::from(block_state).as_registry_block();
542 if !can_use_game_master_blocks(abilities, permission_level)
543 && matches!(
544 registry_block,
545 BlockKind::CommandBlock | BlockKind::StructureBlock
546 )
547 {
548 return;
549 }
550 if block_state == BlockState::AIR {
551 return;
552 }
553
554 let fluid_state = FluidState::from(block_state);
556 let block_state_for_fluid = BlockState::from(fluid_state);
557 let old_state = instance
558 .set_block_state(event.position, block_state_for_fluid)
559 .unwrap_or_default();
560 prediction_handler.retain_known_server_state(event.position, old_state, **player_pos);
561}
562
563#[derive(Message)]
565pub struct StopMiningBlockEvent {
566 pub entity: Entity,
567}
568pub fn handle_stop_mining_block_event(
569 mut events: MessageReader<StopMiningBlockEvent>,
570 mut commands: Commands,
571 mut mine_block_progress_events: MessageWriter<MineBlockProgressEvent>,
572 mut query: Query<(&MineBlockPos, &mut MineProgress)>,
573) {
574 for event in events.read() {
575 let (mine_block_pos, mut mine_progress) = query.get_mut(event.entity).unwrap();
576
577 let mine_block_pos =
578 mine_block_pos.expect("IsMining is true so MineBlockPos must be present");
579 commands.trigger(SendGamePacketEvent::new(
580 event.entity,
581 ServerboundPlayerAction {
582 action: s_player_action::Action::AbortDestroyBlock,
583 pos: mine_block_pos,
584 direction: Direction::Down,
585 seq: 0,
586 },
587 ));
588 commands.entity(event.entity).remove::<Mining>();
589 **mine_progress = 0.;
590 mine_block_progress_events.write(MineBlockProgressEvent {
591 entity: event.entity,
592 position: mine_block_pos,
593 destroy_stage: None,
594 });
595 }
596}
597
598pub fn decrement_mine_delay(mut query: Query<&mut MineDelay>) {
599 for mut mine_delay in &mut query {
600 if **mine_delay > 0 {
601 **mine_delay -= 1;
602 }
603 }
604}
605
606#[allow(clippy::too_many_arguments, clippy::type_complexity)]
607pub fn continue_mining_block(
608 mut query: Query<(
609 Entity,
610 &InstanceName,
611 &LocalGameMode,
612 &Inventory,
613 &MineBlockPos,
614 &MineItem,
615 &ActiveEffects,
616 &FluidOnEyes,
617 &Physics,
618 &Attributes,
619 &Mining,
620 &mut MineDelay,
621 &mut MineProgress,
622 &mut MineTicks,
623 &mut BlockStatePredictionHandler,
624 )>,
625 mut commands: Commands,
626 mut mine_block_progress_events: MessageWriter<MineBlockProgressEvent>,
627 instances: Res<InstanceContainer>,
628) {
629 for (
630 entity,
631 instance_name,
632 game_mode,
633 inventory,
634 current_mining_pos,
635 current_mining_item,
636 active_effects,
637 fluid_on_eyes,
638 physics,
639 attributes,
640 mining,
641 mut mine_delay,
642 mut mine_progress,
643 mut mine_ticks,
644 mut prediction_handler,
645 ) in query.iter_mut()
646 {
647 if game_mode.current == GameMode::Creative {
648 **mine_delay = 5;
650 commands.trigger(SendGamePacketEvent::new(
651 entity,
652 ServerboundPlayerAction {
653 action: s_player_action::Action::StartDestroyBlock,
654 pos: mining.pos,
655 direction: mining.dir,
656 seq: prediction_handler.start_predicting(),
657 },
658 ));
659 commands.trigger(FinishMiningBlockEvent {
660 entity,
661 position: mining.pos,
662 });
663 commands.trigger(SwingArmEvent { entity });
664 } else if mining.force
665 || is_same_mining_target(
666 mining.pos,
667 inventory,
668 current_mining_pos,
669 current_mining_item,
670 )
671 {
672 trace!("continue mining block at {:?}", mining.pos);
673 let instance_lock = instances.get(instance_name).unwrap();
674 let instance = instance_lock.read();
675 let target_block_state = instance.get_block_state(mining.pos).unwrap_or_default();
676
677 trace!("target_block_state: {target_block_state:?}");
678
679 if target_block_state.is_air() {
680 commands.entity(entity).remove::<Mining>();
681 continue;
682 }
683 let block = Box::<dyn BlockTrait>::from(target_block_state);
684 **mine_progress += get_mine_progress(
685 block.as_ref(),
686 current_mining_item.kind(),
687 fluid_on_eyes,
688 physics,
689 attributes,
690 active_effects,
691 );
692
693 if **mine_ticks % 4. == 0. {
694 }
696 **mine_ticks += 1.;
697
698 if **mine_progress >= 1. {
699 commands.entity(entity).remove::<(Mining, MiningQueued)>();
702 trace!("finished mining block at {:?}", mining.pos);
703 commands.trigger(FinishMiningBlockEvent {
704 entity,
705 position: mining.pos,
706 });
707 commands.trigger(SendGamePacketEvent::new(
708 entity,
709 ServerboundPlayerAction {
710 action: s_player_action::Action::StopDestroyBlock,
711 pos: mining.pos,
712 direction: mining.dir,
713 seq: prediction_handler.start_predicting(),
714 },
715 ));
716 **mine_progress = 0.;
717 **mine_ticks = 0.;
718 **mine_delay = 5;
719 }
720
721 mine_block_progress_events.write(MineBlockProgressEvent {
722 entity,
723 position: mining.pos,
724 destroy_stage: mine_progress.destroy_stage(),
725 });
726 commands.trigger(SwingArmEvent { entity });
727 } else {
728 trace!("switching mining target to {:?}", mining.pos);
729 commands.entity(entity).insert(MiningQueued {
730 position: mining.pos,
731 direction: mining.dir,
732 force: false,
733 });
734 }
735 }
736}
737
738pub fn update_mining_component(
739 mut commands: Commands,
740 mut query: Query<(Entity, &mut Mining, &HitResultComponent)>,
741) {
742 for (entity, mut mining, hit_result_component) in &mut query.iter_mut() {
743 if let Some(block_hit_result) = hit_result_component.as_block_hit_result_if_not_miss() {
744 if mining.force && block_hit_result.block_pos != mining.pos {
745 continue;
746 }
747
748 if mining.pos != block_hit_result.block_pos {
749 debug!(
750 "Updating Mining::pos from {:?} to {:?}",
751 mining.pos, block_hit_result.block_pos
752 );
753 mining.pos = block_hit_result.block_pos;
754 }
755 mining.dir = block_hit_result.direction;
756 } else {
757 if mining.force {
758 continue;
759 }
760
761 debug!("Removing mining component because we're no longer looking at the block");
762 commands.entity(entity).remove::<Mining>();
763 }
764 }
765}