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