1use azalea_block::{Block, BlockState, fluid_state::FluidState};
2use azalea_core::{direction::Direction, game_type::GameMode, position::BlockPos, tick::GameTick};
3use azalea_entity::{FluidOnEyes, Physics, 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};
11
12use crate::{
13 Client, InstanceHolder,
14 interact::{
15 CurrentSequenceNumber, HitResultComponent, SwingArmEvent, can_use_game_master_blocks,
16 check_is_interaction_restricted,
17 },
18 inventory::{Inventory, InventorySet},
19 local_player::{LocalGameMode, PermissionLevel, PlayerAbilities},
20 movement::MoveEventsSet,
21 packet::game::SendPacketEvent,
22};
23
24pub struct MiningPlugin;
26impl Plugin for MiningPlugin {
27 fn build(&self, app: &mut App) {
28 app.add_event::<StartMiningBlockEvent>()
29 .add_event::<FinishMiningBlockEvent>()
30 .add_event::<StopMiningBlockEvent>()
31 .add_event::<MineBlockProgressEvent>()
32 .add_event::<AttackBlockEvent>()
33 .add_systems(
34 GameTick,
35 (
36 update_mining_component,
37 continue_mining_block,
38 handle_auto_mine,
39 handle_mining_queued,
40 )
41 .chain()
42 .before(PhysicsSet)
43 .in_set(MiningSet),
44 )
45 .add_systems(
46 Update,
47 (
48 handle_start_mining_block_event,
49 handle_finish_mining_block_event,
50 handle_stop_mining_block_event,
51 )
52 .chain()
53 .in_set(MiningSet)
54 .after(InventorySet)
55 .after(MoveEventsSet)
56 .before(azalea_entity::update_bounding_box)
57 .after(azalea_entity::update_fluid_on_eyes)
58 .after(crate::interact::update_hit_result_component)
59 .after(crate::attack::handle_attack_event)
60 .after(crate::interact::handle_start_use_item_queued)
61 .before(crate::interact::handle_swing_arm_event),
62 );
63 }
64}
65
66#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
68pub struct MiningSet;
69
70impl Client {
71 pub fn start_mining(&self, position: BlockPos) {
72 let mut ecs = self.ecs.lock();
73
74 ecs.send_event(StartMiningBlockEvent {
75 entity: self.entity,
76 position,
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: EventWriter<StartMiningBlockEvent>,
114 mut stop_mining_block_event: EventWriter<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 });
143 } else if mining.is_some() && hit_result_component.is_miss() {
144 stop_mining_block_event.write(StopMiningBlockEvent { entity });
145 }
146 }
147}
148
149#[derive(Component)]
152pub struct Mining {
153 pub pos: BlockPos,
154 pub dir: Direction,
155}
156
157#[derive(Event)]
162pub struct StartMiningBlockEvent {
163 pub entity: Entity,
164 pub position: BlockPos,
165}
166fn handle_start_mining_block_event(
167 mut commands: Commands,
168 mut events: EventReader<StartMiningBlockEvent>,
169 mut query: Query<&HitResultComponent>,
170) {
171 for event in events.read() {
172 let hit_result = query.get_mut(event.entity).unwrap();
173 let direction = if let Some(block_hit_result) = hit_result.as_block_hit_result_if_not_miss()
174 && block_hit_result.block_pos == event.position
175 {
176 block_hit_result.direction
178 } else {
179 Direction::Down
181 };
182 commands.entity(event.entity).insert(MiningQueued {
183 position: event.position,
184 direction,
185 });
186 }
187}
188
189#[derive(Component)]
191pub struct MiningQueued {
192 pub position: BlockPos,
193 pub direction: Direction,
194}
195#[allow(clippy::too_many_arguments, clippy::type_complexity)]
196fn handle_mining_queued(
197 mut commands: Commands,
198 mut finish_mining_events: EventWriter<FinishMiningBlockEvent>,
199 mut attack_block_events: EventWriter<AttackBlockEvent>,
200 mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
201 query: Query<(
202 Entity,
203 &MiningQueued,
204 &InstanceHolder,
205 &LocalGameMode,
206 &Inventory,
207 &FluidOnEyes,
208 &Physics,
209 Option<&Mining>,
210 &mut CurrentSequenceNumber,
211 &mut MineDelay,
212 &mut MineProgress,
213 &mut MineTicks,
214 &mut MineItem,
215 &mut MineBlockPos,
216 )>,
217) {
218 for (
219 entity,
220 mining_queued,
221 instance_holder,
222 game_mode,
223 inventory,
224 fluid_on_eyes,
225 physics,
226 mining,
227 mut sequence_number,
228 mut mine_delay,
229 mut mine_progress,
230 mut mine_ticks,
231 mut current_mining_item,
232 mut current_mining_pos,
233 ) in query
234 {
235 commands.entity(entity).remove::<MiningQueued>();
236
237 let instance = instance_holder.instance.read();
238 if check_is_interaction_restricted(
239 &instance,
240 &mining_queued.position,
241 &game_mode.current,
242 inventory,
243 ) {
244 continue;
245 }
246 if game_mode.current == GameMode::Creative {
250 finish_mining_events.write(FinishMiningBlockEvent {
251 entity,
252 position: mining_queued.position,
253 });
254 **mine_delay = 5;
255 } else if mining.is_none()
256 || !is_same_mining_target(
257 mining_queued.position,
258 inventory,
259 ¤t_mining_pos,
260 ¤t_mining_item,
261 )
262 {
263 if mining.is_some() {
264 commands.trigger(SendPacketEvent::new(
266 entity,
267 ServerboundPlayerAction {
268 action: s_player_action::Action::AbortDestroyBlock,
269 pos: current_mining_pos
270 .expect("IsMining is true so MineBlockPos must be present"),
271 direction: mining_queued.direction,
272 sequence: 0,
273 },
274 ));
275 }
276
277 let target_block_state = instance
278 .get_block_state(&mining_queued.position)
279 .unwrap_or_default();
280
281 let block_is_solid = !target_block_state.outline_shape().is_empty();
283
284 if block_is_solid && **mine_progress == 0. {
285 attack_block_events.write(AttackBlockEvent {
287 entity,
288 position: mining_queued.position,
289 });
290 }
291
292 let block = Box::<dyn Block>::from(target_block_state);
293
294 let held_item = inventory.held_item();
295
296 if block_is_solid
297 && get_mine_progress(
298 block.as_ref(),
299 held_item.kind(),
300 &inventory.inventory_menu,
301 fluid_on_eyes,
302 physics,
303 ) >= 1.
304 {
305 finish_mining_events.write(FinishMiningBlockEvent {
307 entity,
308 position: mining_queued.position,
309 });
310 } else {
311 commands.entity(entity).insert(Mining {
312 pos: mining_queued.position,
313 dir: mining_queued.direction,
314 });
315 **current_mining_pos = Some(mining_queued.position);
316 **current_mining_item = held_item;
317 **mine_progress = 0.;
318 **mine_ticks = 0.;
319 mine_block_progress_events.write(MineBlockProgressEvent {
320 entity,
321 position: mining_queued.position,
322 destroy_stage: mine_progress.destroy_stage(),
323 });
324 }
325
326 commands.trigger(SendPacketEvent::new(
327 entity,
328 ServerboundPlayerAction {
329 action: s_player_action::Action::StartDestroyBlock,
330 pos: mining_queued.position,
331 direction: mining_queued.direction,
332 sequence: sequence_number.get_and_increment(),
333 },
334 ));
335 commands.trigger(SwingArmEvent { entity });
336 commands.trigger(SwingArmEvent { entity });
337 }
338 }
339}
340
341#[derive(Event)]
342pub struct MineBlockProgressEvent {
343 pub entity: Entity,
344 pub position: BlockPos,
345 pub destroy_stage: Option<u32>,
346}
347
348#[derive(Event)]
351pub struct AttackBlockEvent {
352 pub entity: Entity,
353 pub position: BlockPos,
354}
355
356fn is_same_mining_target(
359 target_block: BlockPos,
360 inventory: &Inventory,
361 current_mining_pos: &MineBlockPos,
362 current_mining_item: &MineItem,
363) -> bool {
364 let held_item = inventory.held_item();
365 Some(target_block) == current_mining_pos.0 && held_item == current_mining_item.0
366}
367
368#[derive(Bundle, Default)]
370pub struct MineBundle {
371 pub delay: MineDelay,
372 pub progress: MineProgress,
373 pub ticks: MineTicks,
374 pub mining_pos: MineBlockPos,
375 pub mine_item: MineItem,
376}
377
378#[derive(Component, Debug, Default, Deref, DerefMut)]
380pub struct MineDelay(pub u32);
381
382#[derive(Component, Debug, Default, Deref, DerefMut)]
385pub struct MineProgress(pub f32);
386
387impl MineProgress {
388 pub fn destroy_stage(&self) -> Option<u32> {
389 if self.0 > 0. {
390 Some((self.0 * 10.) as u32)
391 } else {
392 None
393 }
394 }
395}
396
397#[derive(Component, Clone, Debug, Default, Deref, DerefMut)]
401pub struct MineTicks(pub f32);
402
403#[derive(Component, Clone, Debug, Default, Deref, DerefMut)]
405pub struct MineBlockPos(pub Option<BlockPos>);
406
407#[derive(Component, Clone, Debug, Default, Deref, DerefMut)]
410pub struct MineItem(pub ItemStack);
411
412#[derive(Event)]
414pub struct FinishMiningBlockEvent {
415 pub entity: Entity,
416 pub position: BlockPos,
417}
418
419pub fn handle_finish_mining_block_event(
420 mut events: EventReader<FinishMiningBlockEvent>,
421 mut query: Query<(
422 &InstanceName,
423 &LocalGameMode,
424 &Inventory,
425 &PlayerAbilities,
426 &PermissionLevel,
427 &mut CurrentSequenceNumber,
428 )>,
429 instances: Res<InstanceContainer>,
430) {
431 for event in events.read() {
432 let (instance_name, game_mode, inventory, abilities, permission_level, _sequence_number) =
433 query.get_mut(event.entity).unwrap();
434 let instance_lock = instances.get(instance_name).unwrap();
435 let instance = instance_lock.read();
436 if check_is_interaction_restricted(
437 &instance,
438 &event.position,
439 &game_mode.current,
440 inventory,
441 ) {
442 continue;
443 }
444
445 if game_mode.current == GameMode::Creative {
446 let held_item = inventory.held_item().kind();
447 if matches!(
448 held_item,
449 azalea_registry::Item::Trident | azalea_registry::Item::DebugStick
450 ) || azalea_registry::tags::items::SWORDS.contains(&held_item)
451 {
452 continue;
453 }
454 }
455
456 let Some(block_state) = instance.get_block_state(&event.position) else {
457 continue;
458 };
459
460 let registry_block = Box::<dyn Block>::from(block_state).as_registry_block();
461 if !can_use_game_master_blocks(abilities, permission_level)
462 && matches!(
463 registry_block,
464 azalea_registry::Block::CommandBlock | azalea_registry::Block::StructureBlock
465 )
466 {
467 continue;
468 }
469 if block_state == BlockState::AIR {
470 continue;
471 }
472
473 let fluid_state = FluidState::from(block_state);
475 let block_state_for_fluid = BlockState::from(fluid_state);
476 instance.set_block_state(&event.position, block_state_for_fluid);
477 }
478}
479
480#[derive(Event)]
482pub struct StopMiningBlockEvent {
483 pub entity: Entity,
484}
485pub fn handle_stop_mining_block_event(
486 mut events: EventReader<StopMiningBlockEvent>,
487 mut commands: Commands,
488 mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
489 mut query: Query<(&mut Mining, &MineBlockPos, &mut MineProgress)>,
490) {
491 for event in events.read() {
492 let (mut _mining, mine_block_pos, mut mine_progress) = query.get_mut(event.entity).unwrap();
493
494 let mine_block_pos =
495 mine_block_pos.expect("IsMining is true so MineBlockPos must be present");
496 commands.trigger(SendPacketEvent::new(
497 event.entity,
498 ServerboundPlayerAction {
499 action: s_player_action::Action::AbortDestroyBlock,
500 pos: mine_block_pos,
501 direction: Direction::Down,
502 sequence: 0,
503 },
504 ));
505 commands.entity(event.entity).remove::<Mining>();
506 **mine_progress = 0.;
507 mine_block_progress_events.write(MineBlockProgressEvent {
508 entity: event.entity,
509 position: mine_block_pos,
510 destroy_stage: None,
511 });
512 }
513}
514
515#[allow(clippy::too_many_arguments, clippy::type_complexity)]
516pub fn continue_mining_block(
517 mut query: Query<(
518 Entity,
519 &InstanceName,
520 &LocalGameMode,
521 &Inventory,
522 &MineBlockPos,
523 &MineItem,
524 &FluidOnEyes,
525 &Physics,
526 &Mining,
527 &mut MineDelay,
528 &mut MineProgress,
529 &mut MineTicks,
530 &mut CurrentSequenceNumber,
531 )>,
532 mut commands: Commands,
533 mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
534 mut finish_mining_events: EventWriter<FinishMiningBlockEvent>,
535 instances: Res<InstanceContainer>,
536) {
537 for (
538 entity,
539 instance_name,
540 game_mode,
541 inventory,
542 current_mining_pos,
543 current_mining_item,
544 fluid_on_eyes,
545 physics,
546 mining,
547 mut mine_delay,
548 mut mine_progress,
549 mut mine_ticks,
550 mut sequence_number,
551 ) in query.iter_mut()
552 {
553 if **mine_delay > 0 {
554 **mine_delay -= 1;
555 continue;
556 }
557
558 if game_mode.current == GameMode::Creative {
559 **mine_delay = 5;
561 finish_mining_events.write(FinishMiningBlockEvent {
562 entity,
563 position: mining.pos,
564 });
565 commands.trigger(SendPacketEvent::new(
566 entity,
567 ServerboundPlayerAction {
568 action: s_player_action::Action::StartDestroyBlock,
569 pos: mining.pos,
570 direction: mining.dir,
571 sequence: sequence_number.get_and_increment(),
572 },
573 ));
574 commands.trigger(SwingArmEvent { entity });
575 } else if is_same_mining_target(
576 mining.pos,
577 inventory,
578 current_mining_pos,
579 current_mining_item,
580 ) {
581 println!("continue mining block at {:?}", mining.pos);
582 let instance_lock = instances.get(instance_name).unwrap();
583 let instance = instance_lock.read();
584 let target_block_state = instance.get_block_state(&mining.pos).unwrap_or_default();
585
586 println!("target_block_state: {target_block_state:?}");
587
588 if target_block_state.is_air() {
589 commands.entity(entity).remove::<Mining>();
590 continue;
591 }
592 let block = Box::<dyn Block>::from(target_block_state);
593 **mine_progress += get_mine_progress(
594 block.as_ref(),
595 current_mining_item.kind(),
596 &inventory.inventory_menu,
597 fluid_on_eyes,
598 physics,
599 );
600
601 if **mine_ticks % 4. == 0. {
602 }
604 **mine_ticks += 1.;
605
606 if **mine_progress >= 1. {
607 commands.entity(entity).remove::<Mining>();
608 println!("finished mining block at {:?}", mining.pos);
609 finish_mining_events.write(FinishMiningBlockEvent {
610 entity,
611 position: mining.pos,
612 });
613 commands.trigger(SendPacketEvent::new(
614 entity,
615 ServerboundPlayerAction {
616 action: s_player_action::Action::StopDestroyBlock,
617 pos: mining.pos,
618 direction: mining.dir,
619 sequence: sequence_number.get_and_increment(),
620 },
621 ));
622 **mine_progress = 0.;
623 **mine_ticks = 0.;
624 **mine_delay = 0;
625 }
626
627 mine_block_progress_events.write(MineBlockProgressEvent {
628 entity,
629 position: mining.pos,
630 destroy_stage: mine_progress.destroy_stage(),
631 });
632 commands.trigger(SwingArmEvent { entity });
633 } else {
634 println!("switching mining target to {:?}", mining.pos);
635 commands.entity(entity).insert(MiningQueued {
636 position: mining.pos,
637 direction: mining.dir,
638 });
639 }
640 }
641}
642
643pub fn update_mining_component(
644 mut commands: Commands,
645 mut query: Query<(Entity, &mut Mining, &HitResultComponent)>,
646) {
647 for (entity, mut mining, hit_result_component) in &mut query.iter_mut() {
648 if let Some(block_hit_result) = hit_result_component.as_block_hit_result_if_not_miss() {
649 mining.pos = block_hit_result.block_pos;
650 mining.dir = block_hit_result.direction;
651 } else {
652 commands.entity(entity).remove::<Mining>();
653 }
654 }
655}