1use azalea_block::{fluid_state::FluidState, Block, BlockState};
2use azalea_core::{direction::Direction, game_type::GameMode, position::BlockPos, tick::GameTick};
3use azalea_entity::{mining::get_mine_progress, FluidOnEyes, Physics};
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 interact::{
14 can_use_game_master_blocks, check_is_interaction_restricted, CurrentSequenceNumber,
15 HitResultComponent, SwingArmEvent,
16 },
17 inventory::{Inventory, InventorySet},
18 local_player::{LocalGameMode, PermissionLevel, PlayerAbilities},
19 movement::MoveEventsSet,
20 packet_handling::game::SendPacketEvent,
21 Client,
22};
23
24pub struct MinePlugin;
26impl Plugin for MinePlugin {
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)]
63pub struct MiningSet;
64
65impl Client {
66 pub fn start_mining(&mut self, position: BlockPos) {
67 self.ecs.lock().send_event(StartMiningBlockEvent {
68 entity: self.entity,
69 position,
70 });
71 }
72
73 pub fn left_click_mine(&self, enabled: bool) {
76 let mut ecs = self.ecs.lock();
77 let mut entity_mut = ecs.entity_mut(self.entity);
78
79 if enabled {
80 entity_mut.insert(LeftClickMine);
81 } else {
82 entity_mut.remove::<LeftClickMine>();
83 }
84 }
85}
86
87#[derive(Component)]
91pub struct LeftClickMine;
92
93#[allow(clippy::type_complexity)]
94fn handle_auto_mine(
95 mut query: Query<
96 (
97 &HitResultComponent,
98 Entity,
99 Option<&Mining>,
100 &Inventory,
101 &MineBlockPos,
102 &MineItem,
103 ),
104 With<LeftClickMine>,
105 >,
106 mut start_mining_block_event: EventWriter<StartMiningBlockEvent>,
107 mut stop_mining_block_event: EventWriter<StopMiningBlockEvent>,
108) {
109 for (
110 hit_result_component,
111 entity,
112 mining,
113 inventory,
114 current_mining_pos,
115 current_mining_item,
116 ) in &mut query.iter_mut()
117 {
118 let block_pos = hit_result_component.block_pos;
119
120 if (mining.is_none()
121 || !is_same_mining_target(
122 block_pos,
123 inventory,
124 current_mining_pos,
125 current_mining_item,
126 ))
127 && !hit_result_component.miss
128 {
129 start_mining_block_event.send(StartMiningBlockEvent {
130 entity,
131 position: block_pos,
132 });
133 } else if mining.is_some() && hit_result_component.miss {
134 stop_mining_block_event.send(StopMiningBlockEvent { entity });
135 }
136 }
137}
138
139#[derive(Component)]
142pub struct Mining {
143 pub pos: BlockPos,
144 pub dir: Direction,
145}
146
147#[derive(Event)]
152pub struct StartMiningBlockEvent {
153 pub entity: Entity,
154 pub position: BlockPos,
155}
156fn handle_start_mining_block_event(
157 mut events: EventReader<StartMiningBlockEvent>,
158 mut start_mining_events: EventWriter<StartMiningBlockWithDirectionEvent>,
159 mut query: Query<&HitResultComponent>,
160) {
161 for event in events.read() {
162 let hit_result = query.get_mut(event.entity).unwrap();
163 let direction = if hit_result.block_pos == event.position {
164 hit_result.direction
166 } else {
167 Direction::Down
169 };
170 start_mining_events.send(StartMiningBlockWithDirectionEvent {
171 entity: event.entity,
172 position: event.position,
173 direction,
174 });
175 }
176}
177
178#[derive(Event)]
179pub struct StartMiningBlockWithDirectionEvent {
180 pub entity: Entity,
181 pub position: BlockPos,
182 pub direction: Direction,
183}
184#[allow(clippy::too_many_arguments, clippy::type_complexity)]
185fn handle_start_mining_block_with_direction_event(
186 mut events: EventReader<StartMiningBlockWithDirectionEvent>,
187 mut finish_mining_events: EventWriter<FinishMiningBlockEvent>,
188 mut send_packet_events: EventWriter<SendPacketEvent>,
189 mut attack_block_events: EventWriter<AttackBlockEvent>,
190 mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
191 mut query: Query<(
192 &InstanceName,
193 &LocalGameMode,
194 &Inventory,
195 &FluidOnEyes,
196 &Physics,
197 Option<&Mining>,
198 &mut CurrentSequenceNumber,
199 &mut MineDelay,
200 &mut MineProgress,
201 &mut MineTicks,
202 &mut MineItem,
203 &mut MineBlockPos,
204 )>,
205 instances: Res<InstanceContainer>,
206 mut commands: Commands,
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 send_packet_events.send(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 send_packet_events.send(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 send_packet_events: EventWriter<SendPacketEvent>,
486 mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
487 mut query: Query<(&mut Mining, &MineBlockPos, &mut MineProgress)>,
488 mut commands: Commands,
489) {
490 for event in events.read() {
491 let (mut _mining, mine_block_pos, mut mine_progress) = query.get_mut(event.entity).unwrap();
492
493 let mine_block_pos =
494 mine_block_pos.expect("IsMining is true so MineBlockPos must be present");
495 send_packet_events.send(SendPacketEvent::new(
496 event.entity,
497 ServerboundPlayerAction {
498 action: s_player_action::Action::AbortDestroyBlock,
499 pos: mine_block_pos,
500 direction: Direction::Down,
501 sequence: 0,
502 },
503 ));
504 commands.entity(event.entity).remove::<Mining>();
505 **mine_progress = 0.;
506 mine_block_progress_events.send(MineBlockProgressEvent {
507 entity: event.entity,
508 position: mine_block_pos,
509 destroy_stage: None,
510 });
511 }
512}
513
514#[allow(clippy::too_many_arguments, clippy::type_complexity)]
515pub fn continue_mining_block(
516 mut query: Query<(
517 Entity,
518 &InstanceName,
519 &LocalGameMode,
520 &Inventory,
521 &MineBlockPos,
522 &MineItem,
523 &FluidOnEyes,
524 &Physics,
525 &Mining,
526 &mut MineDelay,
527 &mut MineProgress,
528 &mut MineTicks,
529 &mut CurrentSequenceNumber,
530 )>,
531 mut send_packet_events: EventWriter<SendPacketEvent>,
532 mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
533 mut finish_mining_events: EventWriter<FinishMiningBlockEvent>,
534 mut start_mining_events: EventWriter<StartMiningBlockWithDirectionEvent>,
535 mut swing_arm_events: EventWriter<SwingArmEvent>,
536 instances: Res<InstanceContainer>,
537 mut commands: Commands,
538) {
539 for (
540 entity,
541 instance_name,
542 game_mode,
543 inventory,
544 current_mining_pos,
545 current_mining_item,
546 fluid_on_eyes,
547 physics,
548 mining,
549 mut mine_delay,
550 mut mine_progress,
551 mut mine_ticks,
552 mut sequence_number,
553 ) in query.iter_mut()
554 {
555 if **mine_delay > 0 {
556 **mine_delay -= 1;
557 continue;
558 }
559
560 if game_mode.current == GameMode::Creative {
561 **mine_delay = 5;
563 finish_mining_events.send(FinishMiningBlockEvent {
564 entity,
565 position: mining.pos,
566 });
567 *sequence_number += 1;
568 send_packet_events.send(SendPacketEvent::new(
569 entity,
570 ServerboundPlayerAction {
571 action: s_player_action::Action::StartDestroyBlock,
572 pos: mining.pos,
573 direction: mining.dir,
574 sequence: **sequence_number,
575 },
576 ));
577 swing_arm_events.send(SwingArmEvent { entity });
578 } else if is_same_mining_target(
579 mining.pos,
580 inventory,
581 current_mining_pos,
582 current_mining_item,
583 ) {
584 let instance_lock = instances.get(instance_name).unwrap();
585 let instance = instance_lock.read();
586 let target_block_state = instance.get_block_state(&mining.pos).unwrap_or_default();
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 *sequence_number += 1;
609 finish_mining_events.send(FinishMiningBlockEvent {
610 entity,
611 position: mining.pos,
612 });
613 send_packet_events.send(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,
620 },
621 ));
622 **mine_progress = 0.;
623 **mine_ticks = 0.;
624 **mine_delay = 0;
625 }
626
627 mine_block_progress_events.send(MineBlockProgressEvent {
628 entity,
629 position: mining.pos,
630 destroy_stage: mine_progress.destroy_stage(),
631 });
632 swing_arm_events.send(SwingArmEvent { entity });
633 } else {
634 start_mining_events.send(StartMiningBlockWithDirectionEvent {
635 entity,
636 position: mining.pos,
637 direction: mining.dir,
638 });
639 }
640
641 swing_arm_events.send(SwingArmEvent { entity });
642 }
643}