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;
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,
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::<StartMiningBlockWithDirectionEvent>()
30 .add_event::<FinishMiningBlockEvent>()
31 .add_event::<StopMiningBlockEvent>()
32 .add_event::<MineBlockProgressEvent>()
33 .add_event::<AttackBlockEvent>()
34 .add_systems(
35 GameTick,
36 (continue_mining_block, handle_auto_mine)
37 .chain()
38 .before(PhysicsSet),
39 )
40 .add_systems(
41 Update,
42 (
43 handle_start_mining_block_event,
44 handle_start_mining_block_with_direction_event,
45 handle_finish_mining_block_event,
46 handle_stop_mining_block_event,
47 )
48 .chain()
49 .in_set(MiningSet)
50 .after(InventorySet)
51 .after(MoveEventsSet)
52 .before(azalea_entity::update_bounding_box)
53 .after(azalea_entity::update_fluid_on_eyes)
54 .after(crate::interact::update_hit_result_component)
55 .after(crate::attack::handle_attack_event)
56 .after(crate::interact::handle_block_interact_event)
57 .before(crate::interact::handle_swing_arm_event),
58 );
59 }
60}
61
62#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
64pub struct MiningSet;
65
66impl Client {
67 pub fn start_mining(&mut self, position: BlockPos) {
68 self.ecs.lock().send_event(StartMiningBlockEvent {
69 entity: self.entity,
70 position,
71 });
72 }
73
74 pub fn left_click_mine(&self, enabled: bool) {
77 let mut ecs = self.ecs.lock();
78 let mut entity_mut = ecs.entity_mut(self.entity);
79
80 if enabled {
81 entity_mut.insert(LeftClickMine);
82 } else {
83 entity_mut.remove::<LeftClickMine>();
84 }
85 }
86}
87
88#[derive(Component)]
92pub struct LeftClickMine;
93
94#[allow(clippy::type_complexity)]
95fn handle_auto_mine(
96 mut query: Query<
97 (
98 &HitResultComponent,
99 Entity,
100 Option<&Mining>,
101 &Inventory,
102 &MineBlockPos,
103 &MineItem,
104 ),
105 With<LeftClickMine>,
106 >,
107 mut start_mining_block_event: EventWriter<StartMiningBlockEvent>,
108 mut stop_mining_block_event: EventWriter<StopMiningBlockEvent>,
109) {
110 for (
111 hit_result_component,
112 entity,
113 mining,
114 inventory,
115 current_mining_pos,
116 current_mining_item,
117 ) in &mut query.iter_mut()
118 {
119 let block_pos = hit_result_component.block_pos;
120
121 if (mining.is_none()
122 || !is_same_mining_target(
123 block_pos,
124 inventory,
125 current_mining_pos,
126 current_mining_item,
127 ))
128 && !hit_result_component.miss
129 {
130 start_mining_block_event.send(StartMiningBlockEvent {
131 entity,
132 position: block_pos,
133 });
134 } else if mining.is_some() && hit_result_component.miss {
135 stop_mining_block_event.send(StopMiningBlockEvent { entity });
136 }
137 }
138}
139
140#[derive(Component)]
143pub struct Mining {
144 pub pos: BlockPos,
145 pub dir: Direction,
146}
147
148#[derive(Event)]
153pub struct StartMiningBlockEvent {
154 pub entity: Entity,
155 pub position: BlockPos,
156}
157fn handle_start_mining_block_event(
158 mut events: EventReader<StartMiningBlockEvent>,
159 mut start_mining_events: EventWriter<StartMiningBlockWithDirectionEvent>,
160 mut query: Query<&HitResultComponent>,
161) {
162 for event in events.read() {
163 let hit_result = query.get_mut(event.entity).unwrap();
164 let direction = if hit_result.block_pos == event.position {
165 hit_result.direction
167 } else {
168 Direction::Down
170 };
171 start_mining_events.send(StartMiningBlockWithDirectionEvent {
172 entity: event.entity,
173 position: event.position,
174 direction,
175 });
176 }
177}
178
179#[derive(Event)]
180pub struct StartMiningBlockWithDirectionEvent {
181 pub entity: Entity,
182 pub position: BlockPos,
183 pub direction: Direction,
184}
185#[allow(clippy::too_many_arguments, clippy::type_complexity)]
186fn handle_start_mining_block_with_direction_event(
187 mut events: EventReader<StartMiningBlockWithDirectionEvent>,
188 mut finish_mining_events: EventWriter<FinishMiningBlockEvent>,
189 mut commands: Commands,
190 mut attack_block_events: EventWriter<AttackBlockEvent>,
191 mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
192 mut query: Query<(
193 &InstanceName,
194 &LocalGameMode,
195 &Inventory,
196 &FluidOnEyes,
197 &Physics,
198 Option<&Mining>,
199 &mut CurrentSequenceNumber,
200 &mut MineDelay,
201 &mut MineProgress,
202 &mut MineTicks,
203 &mut MineItem,
204 &mut MineBlockPos,
205 )>,
206 instances: Res<InstanceContainer>,
207) {
208 for event in events.read() {
209 let (
210 instance_name,
211 game_mode,
212 inventory,
213 fluid_on_eyes,
214 physics,
215 mining,
216 mut sequence_number,
217 mut mine_delay,
218 mut mine_progress,
219 mut mine_ticks,
220 mut current_mining_item,
221 mut current_mining_pos,
222 ) = query.get_mut(event.entity).unwrap();
223
224 let instance_lock = instances.get(instance_name).unwrap();
225 let instance = instance_lock.read();
226 if check_is_interaction_restricted(
227 &instance,
228 &event.position,
229 &game_mode.current,
230 inventory,
231 ) {
232 continue;
233 }
234 if game_mode.current == GameMode::Creative {
238 *sequence_number += 1;
239 finish_mining_events.send(FinishMiningBlockEvent {
240 entity: event.entity,
241 position: event.position,
242 });
243 **mine_delay = 5;
244 } else if mining.is_none()
245 || !is_same_mining_target(
246 event.position,
247 inventory,
248 ¤t_mining_pos,
249 ¤t_mining_item,
250 )
251 {
252 if mining.is_some() {
253 commands.trigger(SendPacketEvent::new(
255 event.entity,
256 ServerboundPlayerAction {
257 action: s_player_action::Action::AbortDestroyBlock,
258 pos: current_mining_pos
259 .expect("IsMining is true so MineBlockPos must be present"),
260 direction: event.direction,
261 sequence: 0,
262 },
263 ));
264 }
265
266 let target_block_state = instance
267 .get_block_state(&event.position)
268 .unwrap_or_default();
269 *sequence_number += 1;
270 let target_registry_block = azalea_registry::Block::from(target_block_state);
271
272 let block_is_solid = !target_block_state.is_air()
278 && !matches!(
280 target_registry_block,
281 azalea_registry::Block::Water | azalea_registry::Block::Lava
282 );
283
284 if block_is_solid && **mine_progress == 0. {
285 attack_block_events.send(AttackBlockEvent {
287 entity: event.entity,
288 position: event.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.send(FinishMiningBlockEvent {
307 entity: event.entity,
308 position: event.position,
309 });
310 } else {
311 commands.entity(event.entity).insert(Mining {
312 pos: event.position,
313 dir: event.direction,
314 });
315 **current_mining_pos = Some(event.position);
316 **current_mining_item = held_item;
317 **mine_progress = 0.;
318 **mine_ticks = 0.;
319 mine_block_progress_events.send(MineBlockProgressEvent {
320 entity: event.entity,
321 position: event.position,
322 destroy_stage: mine_progress.destroy_stage(),
323 });
324 }
325
326 commands.trigger(SendPacketEvent::new(
327 event.entity,
328 ServerboundPlayerAction {
329 action: s_player_action::Action::StartDestroyBlock,
330 pos: event.position,
331 direction: event.direction,
332 sequence: **sequence_number,
333 },
334 ));
335 }
336 }
337}
338
339#[derive(Event)]
340pub struct MineBlockProgressEvent {
341 pub entity: Entity,
342 pub position: BlockPos,
343 pub destroy_stage: Option<u32>,
344}
345
346#[derive(Event)]
349pub struct AttackBlockEvent {
350 pub entity: Entity,
351 pub position: BlockPos,
352}
353
354fn is_same_mining_target(
357 target_block: BlockPos,
358 inventory: &Inventory,
359 current_mining_pos: &MineBlockPos,
360 current_mining_item: &MineItem,
361) -> bool {
362 let held_item = inventory.held_item();
363 Some(target_block) == current_mining_pos.0 && held_item == current_mining_item.0
364}
365
366#[derive(Bundle, Default)]
368pub struct MineBundle {
369 pub delay: MineDelay,
370 pub progress: MineProgress,
371 pub ticks: MineTicks,
372 pub mining_pos: MineBlockPos,
373 pub mine_item: MineItem,
374}
375
376#[derive(Component, Debug, Default, Deref, DerefMut)]
378pub struct MineDelay(pub u32);
379
380#[derive(Component, Debug, Default, Deref, DerefMut)]
383pub struct MineProgress(pub f32);
384
385impl MineProgress {
386 pub fn destroy_stage(&self) -> Option<u32> {
387 if self.0 > 0. {
388 Some((self.0 * 10.) as u32)
389 } else {
390 None
391 }
392 }
393}
394
395#[derive(Component, Clone, Debug, Default, Deref, DerefMut)]
399pub struct MineTicks(pub f32);
400
401#[derive(Component, Clone, Debug, Default, Deref, DerefMut)]
403pub struct MineBlockPos(pub Option<BlockPos>);
404
405#[derive(Component, Clone, Debug, Default, Deref, DerefMut)]
408pub struct MineItem(pub ItemStack);
409
410#[derive(Event)]
412pub struct FinishMiningBlockEvent {
413 pub entity: Entity,
414 pub position: BlockPos,
415}
416
417pub fn handle_finish_mining_block_event(
418 mut events: EventReader<FinishMiningBlockEvent>,
419 mut query: Query<(
420 &InstanceName,
421 &LocalGameMode,
422 &Inventory,
423 &PlayerAbilities,
424 &PermissionLevel,
425 &mut CurrentSequenceNumber,
426 )>,
427 instances: Res<InstanceContainer>,
428) {
429 for event in events.read() {
430 let (instance_name, game_mode, inventory, abilities, permission_level, _sequence_number) =
431 query.get_mut(event.entity).unwrap();
432 let instance_lock = instances.get(instance_name).unwrap();
433 let instance = instance_lock.read();
434 if check_is_interaction_restricted(
435 &instance,
436 &event.position,
437 &game_mode.current,
438 inventory,
439 ) {
440 continue;
441 }
442
443 if game_mode.current == GameMode::Creative {
444 let held_item = inventory.held_item().kind();
445 if matches!(
446 held_item,
447 azalea_registry::Item::Trident | azalea_registry::Item::DebugStick
448 ) || azalea_registry::tags::items::SWORDS.contains(&held_item)
449 {
450 continue;
451 }
452 }
453
454 let Some(block_state) = instance.get_block_state(&event.position) else {
455 continue;
456 };
457
458 let registry_block = Box::<dyn Block>::from(block_state).as_registry_block();
459 if !can_use_game_master_blocks(abilities, permission_level)
460 && matches!(
461 registry_block,
462 azalea_registry::Block::CommandBlock | azalea_registry::Block::StructureBlock
463 )
464 {
465 continue;
466 }
467 if block_state == BlockState::AIR {
468 continue;
469 }
470
471 let fluid_state = FluidState::from(block_state);
473 let block_state_for_fluid = BlockState::from(fluid_state);
474 instance.set_block_state(&event.position, block_state_for_fluid);
475 }
476}
477
478#[derive(Event)]
480pub struct StopMiningBlockEvent {
481 pub entity: Entity,
482}
483pub fn handle_stop_mining_block_event(
484 mut events: EventReader<StopMiningBlockEvent>,
485 mut commands: Commands,
486 mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
487 mut query: Query<(&mut Mining, &MineBlockPos, &mut MineProgress)>,
488) {
489 for event in events.read() {
490 let (mut _mining, mine_block_pos, mut mine_progress) = query.get_mut(event.entity).unwrap();
491
492 let mine_block_pos =
493 mine_block_pos.expect("IsMining is true so MineBlockPos must be present");
494 commands.trigger(SendPacketEvent::new(
495 event.entity,
496 ServerboundPlayerAction {
497 action: s_player_action::Action::AbortDestroyBlock,
498 pos: mine_block_pos,
499 direction: Direction::Down,
500 sequence: 0,
501 },
502 ));
503 commands.entity(event.entity).remove::<Mining>();
504 **mine_progress = 0.;
505 mine_block_progress_events.send(MineBlockProgressEvent {
506 entity: event.entity,
507 position: mine_block_pos,
508 destroy_stage: None,
509 });
510 }
511}
512
513#[allow(clippy::too_many_arguments, clippy::type_complexity)]
514pub fn continue_mining_block(
515 mut query: Query<(
516 Entity,
517 &InstanceName,
518 &LocalGameMode,
519 &Inventory,
520 &MineBlockPos,
521 &MineItem,
522 &FluidOnEyes,
523 &Physics,
524 &Mining,
525 &mut MineDelay,
526 &mut MineProgress,
527 &mut MineTicks,
528 &mut CurrentSequenceNumber,
529 )>,
530 mut commands: Commands,
531 mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
532 mut finish_mining_events: EventWriter<FinishMiningBlockEvent>,
533 mut start_mining_events: EventWriter<StartMiningBlockWithDirectionEvent>,
534 mut swing_arm_events: EventWriter<SwingArmEvent>,
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.send(FinishMiningBlockEvent {
562 entity,
563 position: mining.pos,
564 });
565 *sequence_number += 1;
566 commands.trigger(SendPacketEvent::new(
567 entity,
568 ServerboundPlayerAction {
569 action: s_player_action::Action::StartDestroyBlock,
570 pos: mining.pos,
571 direction: mining.dir,
572 sequence: **sequence_number,
573 },
574 ));
575 swing_arm_events.send(SwingArmEvent { entity });
576 } else if is_same_mining_target(
577 mining.pos,
578 inventory,
579 current_mining_pos,
580 current_mining_item,
581 ) {
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 if target_block_state.is_air() {
587 commands.entity(entity).remove::<Mining>();
588 continue;
589 }
590 let block = Box::<dyn Block>::from(target_block_state);
591 **mine_progress += get_mine_progress(
592 block.as_ref(),
593 current_mining_item.kind(),
594 &inventory.inventory_menu,
595 fluid_on_eyes,
596 physics,
597 );
598
599 if **mine_ticks % 4. == 0. {
600 }
602 **mine_ticks += 1.;
603
604 if **mine_progress >= 1. {
605 commands.entity(entity).remove::<Mining>();
606 *sequence_number += 1;
607 finish_mining_events.send(FinishMiningBlockEvent {
608 entity,
609 position: mining.pos,
610 });
611 commands.trigger(SendPacketEvent::new(
612 entity,
613 ServerboundPlayerAction {
614 action: s_player_action::Action::StopDestroyBlock,
615 pos: mining.pos,
616 direction: mining.dir,
617 sequence: **sequence_number,
618 },
619 ));
620 **mine_progress = 0.;
621 **mine_ticks = 0.;
622 **mine_delay = 0;
623 }
624
625 mine_block_progress_events.send(MineBlockProgressEvent {
626 entity,
627 position: mining.pos,
628 destroy_stage: mine_progress.destroy_stage(),
629 });
630 swing_arm_events.send(SwingArmEvent { entity });
631 } else {
632 start_mining_events.send(StartMiningBlockWithDirectionEvent {
633 entity,
634 position: mining.pos,
635 direction: mining.dir,
636 });
637 }
638
639 swing_arm_events.send(SwingArmEvent { entity });
640 }
641}