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