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