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::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 continue_mining_block,
39 handle_auto_mine,
40 handle_mining_queued,
41 )
42 .chain()
43 .after(PhysicsSet)
44 .after(super::movement::send_position)
45 .after(super::attack::handle_attack_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 (Direction::Down, true)
186 };
187 commands.entity(event.entity).insert(MiningQueued {
188 position: event.position,
189 direction,
190 force,
191 });
192 }
193}
194
195#[derive(Component, Debug, Clone)]
197pub struct MiningQueued {
198 pub position: BlockPos,
199 pub direction: Direction,
200 pub force: bool,
202}
203#[allow(clippy::too_many_arguments, clippy::type_complexity)]
204fn handle_mining_queued(
205 mut commands: Commands,
206 mut attack_block_events: EventWriter<AttackBlockEvent>,
207 mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
208 query: Query<(
209 Entity,
210 &MiningQueued,
211 &InstanceHolder,
212 &LocalGameMode,
213 &Inventory,
214 &FluidOnEyes,
215 &Physics,
216 Option<&Mining>,
217 &mut BlockStatePredictionHandler,
218 &mut MineDelay,
219 &mut MineProgress,
220 &mut MineTicks,
221 &mut MineItem,
222 &mut MineBlockPos,
223 )>,
224) {
225 for (
226 entity,
227 mining_queued,
228 instance_holder,
229 game_mode,
230 inventory,
231 fluid_on_eyes,
232 physics,
233 mining,
234 mut sequence_number,
235 mut mine_delay,
236 mut mine_progress,
237 mut mine_ticks,
238 mut current_mining_item,
239 mut current_mining_pos,
240 ) in query
241 {
242 commands.entity(entity).remove::<MiningQueued>();
243
244 let instance = instance_holder.instance.read();
245 if check_is_interaction_restricted(
246 &instance,
247 mining_queued.position,
248 &game_mode.current,
249 inventory,
250 ) {
251 continue;
252 }
253 if game_mode.current == GameMode::Creative {
257 commands.trigger(SendPacketEvent::new(
260 entity,
261 ServerboundPlayerAction {
262 action: s_player_action::Action::StartDestroyBlock,
263 pos: mining_queued.position,
264 direction: mining_queued.direction,
265 seq: sequence_number.start_predicting(),
266 },
267 ));
268 commands.trigger_targets(
269 FinishMiningBlockEvent {
270 position: mining_queued.position,
271 },
272 entity,
273 );
274 **mine_delay = 5;
275 commands.trigger(SwingArmEvent { entity });
276 } else if mining.is_none()
277 || !is_same_mining_target(
278 mining_queued.position,
279 inventory,
280 ¤t_mining_pos,
281 ¤t_mining_item,
282 )
283 {
284 if mining.is_some() {
285 commands.trigger(SendPacketEvent::new(
287 entity,
288 ServerboundPlayerAction {
289 action: s_player_action::Action::AbortDestroyBlock,
290 pos: current_mining_pos
291 .expect("IsMining is true so MineBlockPos must be present"),
292 direction: mining_queued.direction,
293 seq: 0,
294 },
295 ));
296 }
297
298 let target_block_state = instance
299 .get_block_state(mining_queued.position)
300 .unwrap_or_default();
301
302 let block_is_solid = !target_block_state.outline_shape().is_empty();
304
305 if block_is_solid && **mine_progress == 0. {
306 attack_block_events.write(AttackBlockEvent {
308 entity,
309 position: mining_queued.position,
310 });
311 }
312
313 let block = Box::<dyn BlockTrait>::from(target_block_state);
314
315 let held_item = inventory.held_item();
316
317 if block_is_solid
318 && get_mine_progress(
319 block.as_ref(),
320 held_item.kind(),
321 &inventory.inventory_menu,
322 fluid_on_eyes,
323 physics,
324 ) >= 1.
325 {
326 commands.trigger_targets(
328 FinishMiningBlockEvent {
329 position: mining_queued.position,
330 },
331 entity,
332 );
333 } else {
334 let mining = Mining {
335 pos: mining_queued.position,
336 dir: mining_queued.direction,
337 force: mining_queued.force,
338 };
339 trace!("inserting mining component {mining:?} for entity {entity:?}");
340 commands.entity(entity).insert(mining);
341 **current_mining_pos = Some(mining_queued.position);
342 **current_mining_item = held_item;
343 **mine_progress = 0.;
344 **mine_ticks = 0.;
345 mine_block_progress_events.write(MineBlockProgressEvent {
346 entity,
347 position: mining_queued.position,
348 destroy_stage: mine_progress.destroy_stage(),
349 });
350 }
351
352 commands.trigger(SendPacketEvent::new(
353 entity,
354 ServerboundPlayerAction {
355 action: s_player_action::Action::StartDestroyBlock,
356 pos: mining_queued.position,
357 direction: mining_queued.direction,
358 seq: sequence_number.start_predicting(),
359 },
360 ));
361 commands.trigger(SwingArmEvent { entity });
363 commands.trigger(SwingArmEvent { entity });
364 }
365 }
366}
367
368#[derive(Event)]
369pub struct MineBlockProgressEvent {
370 pub entity: Entity,
371 pub position: BlockPos,
372 pub destroy_stage: Option<u32>,
373}
374
375#[derive(Event)]
378pub struct AttackBlockEvent {
379 pub entity: Entity,
380 pub position: BlockPos,
381}
382
383fn is_same_mining_target(
386 target_block: BlockPos,
387 inventory: &Inventory,
388 current_mining_pos: &MineBlockPos,
389 current_mining_item: &MineItem,
390) -> bool {
391 let held_item = inventory.held_item();
392 Some(target_block) == current_mining_pos.0 && held_item == current_mining_item.0
393}
394
395#[derive(Bundle, Default, Clone)]
397pub struct MineBundle {
398 pub delay: MineDelay,
399 pub progress: MineProgress,
400 pub ticks: MineTicks,
401 pub mining_pos: MineBlockPos,
402 pub mine_item: MineItem,
403}
404
405#[derive(Component, Debug, Default, Deref, DerefMut, Clone)]
407pub struct MineDelay(pub u32);
408
409#[derive(Component, Debug, Default, Deref, DerefMut, Clone)]
412pub struct MineProgress(pub f32);
413
414impl MineProgress {
415 pub fn destroy_stage(&self) -> Option<u32> {
416 if self.0 > 0. {
417 Some((self.0 * 10.) as u32)
418 } else {
419 None
420 }
421 }
422}
423
424#[derive(Component, Clone, Debug, Default, Deref, DerefMut)]
428pub struct MineTicks(pub f32);
429
430#[derive(Component, Clone, Debug, Default, Deref, DerefMut)]
432pub struct MineBlockPos(pub Option<BlockPos>);
433
434#[derive(Component, Clone, Debug, Default, Deref, DerefMut)]
437pub struct MineItem(pub ItemStack);
438
439#[derive(Event)]
441pub struct FinishMiningBlockEvent {
442 pub position: BlockPos,
443}
444
445pub fn handle_finish_mining_block_observer(
446 trigger: Trigger<FinishMiningBlockEvent>,
447 mut query: Query<(
448 &InstanceName,
449 &LocalGameMode,
450 &Inventory,
451 &PlayerAbilities,
452 &PermissionLevel,
453 &Position,
454 &mut BlockStatePredictionHandler,
455 )>,
456 instances: Res<InstanceContainer>,
457) {
458 let event = trigger.event();
459
460 let (
461 instance_name,
462 game_mode,
463 inventory,
464 abilities,
465 permission_level,
466 player_pos,
467 mut prediction_handler,
468 ) = query.get_mut(trigger.target()).unwrap();
469 let instance_lock = instances.get(instance_name).unwrap();
470 let instance = instance_lock.read();
471 if check_is_interaction_restricted(&instance, event.position, &game_mode.current, inventory) {
472 return;
473 }
474
475 if game_mode.current == GameMode::Creative {
476 let held_item = inventory.held_item().kind();
477 if matches!(
478 held_item,
479 azalea_registry::Item::Trident | azalea_registry::Item::DebugStick
480 ) || azalea_registry::tags::items::SWORDS.contains(&held_item)
481 {
482 return;
483 }
484 }
485
486 let Some(block_state) = instance.get_block_state(event.position) else {
487 return;
488 };
489
490 let registry_block: azalea_registry::Block =
491 Box::<dyn BlockTrait>::from(block_state).as_registry_block();
492 if !can_use_game_master_blocks(abilities, permission_level)
493 && matches!(
494 registry_block,
495 azalea_registry::Block::CommandBlock | azalea_registry::Block::StructureBlock
496 )
497 {
498 return;
499 }
500 if block_state == BlockState::AIR {
501 return;
502 }
503
504 let fluid_state = FluidState::from(block_state);
506 let block_state_for_fluid = BlockState::from(fluid_state);
507 let old_state = instance
508 .set_block_state(event.position, block_state_for_fluid)
509 .unwrap_or_default();
510 prediction_handler.retain_known_server_state(event.position, old_state, **player_pos);
511}
512
513#[derive(Event)]
515pub struct StopMiningBlockEvent {
516 pub entity: Entity,
517}
518pub fn handle_stop_mining_block_event(
519 mut events: EventReader<StopMiningBlockEvent>,
520 mut commands: Commands,
521 mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
522 mut query: Query<(&MineBlockPos, &mut MineProgress)>,
523) {
524 for event in events.read() {
525 let (mine_block_pos, mut mine_progress) = query.get_mut(event.entity).unwrap();
526
527 let mine_block_pos =
528 mine_block_pos.expect("IsMining is true so MineBlockPos must be present");
529 commands.trigger(SendPacketEvent::new(
530 event.entity,
531 ServerboundPlayerAction {
532 action: s_player_action::Action::AbortDestroyBlock,
533 pos: mine_block_pos,
534 direction: Direction::Down,
535 seq: 0,
536 },
537 ));
538 commands.entity(event.entity).remove::<Mining>();
539 **mine_progress = 0.;
540 mine_block_progress_events.write(MineBlockProgressEvent {
541 entity: event.entity,
542 position: mine_block_pos,
543 destroy_stage: None,
544 });
545 }
546}
547
548#[allow(clippy::too_many_arguments, clippy::type_complexity)]
549pub fn continue_mining_block(
550 mut query: Query<(
551 Entity,
552 &InstanceName,
553 &LocalGameMode,
554 &Inventory,
555 &MineBlockPos,
556 &MineItem,
557 &FluidOnEyes,
558 &Physics,
559 &Mining,
560 &mut MineDelay,
561 &mut MineProgress,
562 &mut MineTicks,
563 &mut BlockStatePredictionHandler,
564 )>,
565 mut commands: Commands,
566 mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
567 instances: Res<InstanceContainer>,
568) {
569 for (
570 entity,
571 instance_name,
572 game_mode,
573 inventory,
574 current_mining_pos,
575 current_mining_item,
576 fluid_on_eyes,
577 physics,
578 mining,
579 mut mine_delay,
580 mut mine_progress,
581 mut mine_ticks,
582 mut prediction_handler,
583 ) in query.iter_mut()
584 {
585 if **mine_delay > 0 {
586 **mine_delay -= 1;
587 continue;
588 }
589
590 if game_mode.current == GameMode::Creative {
591 **mine_delay = 5;
593 commands.trigger(SendPacketEvent::new(
594 entity,
595 ServerboundPlayerAction {
596 action: s_player_action::Action::StartDestroyBlock,
597 pos: mining.pos,
598 direction: mining.dir,
599 seq: prediction_handler.start_predicting(),
600 },
601 ));
602 commands.trigger_targets(
603 FinishMiningBlockEvent {
604 position: mining.pos,
605 },
606 entity,
607 );
608 commands.trigger(SwingArmEvent { entity });
609 } else if mining.force
610 || is_same_mining_target(
611 mining.pos,
612 inventory,
613 current_mining_pos,
614 current_mining_item,
615 )
616 {
617 trace!("continue mining block at {:?}", mining.pos);
618 let instance_lock = instances.get(instance_name).unwrap();
619 let instance = instance_lock.read();
620 let target_block_state = instance.get_block_state(mining.pos).unwrap_or_default();
621
622 trace!("target_block_state: {target_block_state:?}");
623
624 if target_block_state.is_air() {
625 commands.entity(entity).remove::<Mining>();
626 continue;
627 }
628 let block = Box::<dyn BlockTrait>::from(target_block_state);
629 **mine_progress += get_mine_progress(
630 block.as_ref(),
631 current_mining_item.kind(),
632 &inventory.inventory_menu,
633 fluid_on_eyes,
634 physics,
635 );
636
637 if **mine_ticks % 4. == 0. {
638 }
640 **mine_ticks += 1.;
641
642 if **mine_progress >= 1. {
643 commands.entity(entity).remove::<(Mining, MiningQueued)>();
646 trace!("finished mining block at {:?}", mining.pos);
647 commands.trigger_targets(
648 FinishMiningBlockEvent {
649 position: mining.pos,
650 },
651 entity,
652 );
653 commands.trigger(SendPacketEvent::new(
654 entity,
655 ServerboundPlayerAction {
656 action: s_player_action::Action::StopDestroyBlock,
657 pos: mining.pos,
658 direction: mining.dir,
659 seq: prediction_handler.start_predicting(),
660 },
661 ));
662 **mine_progress = 0.;
663 **mine_ticks = 0.;
664 **mine_delay = 5;
665 }
666
667 mine_block_progress_events.write(MineBlockProgressEvent {
668 entity,
669 position: mining.pos,
670 destroy_stage: mine_progress.destroy_stage(),
671 });
672 commands.trigger(SwingArmEvent { entity });
673 } else {
674 trace!("switching mining target to {:?}", mining.pos);
675 commands.entity(entity).insert(MiningQueued {
676 position: mining.pos,
677 direction: mining.dir,
678 force: false,
679 });
680 }
681 }
682}
683
684pub fn update_mining_component(
685 mut commands: Commands,
686 mut query: Query<(Entity, &mut Mining, &HitResultComponent)>,
687) {
688 for (entity, mut mining, hit_result_component) in &mut query.iter_mut() {
689 if let Some(block_hit_result) = hit_result_component.as_block_hit_result_if_not_miss() {
690 mining.pos = block_hit_result.block_pos;
691 mining.dir = block_hit_result.direction;
692 } else {
693 commands.entity(entity).remove::<Mining>();
694 }
695 }
696}