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