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