aboutsummaryrefslogtreecommitdiff
path: root/azalea-client/src/plugins/mining.rs
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2025-02-22 21:45:26 -0600
committerGitHub <noreply@github.com>2025-02-22 21:45:26 -0600
commite21e1b97bf9337e9f4747cd1b545b1b3a03e2ce7 (patch)
treeadd6f8bfce40d0c07845d8aa4c9945a0b918444c /azalea-client/src/plugins/mining.rs
parentf8130c3c92946d2293634ba4e252d6bc93026c3c (diff)
downloadazalea-drasl-e21e1b97bf9337e9f4747cd1b545b1b3a03e2ce7.tar.xz
Refactor azalea-client (#205)
* start organizing packet_handling more by moving packet handlers into their own functions * finish writing all the handler functions for packets * use macro for generating match statement for packet handler functions * fix set_entity_data * update config state to also use handler functions * organize az-client file structure by moving things into plugins directory * fix merge issues
Diffstat (limited to 'azalea-client/src/plugins/mining.rs')
-rw-r--r--azalea-client/src/plugins/mining.rs644
1 files changed, 644 insertions, 0 deletions
diff --git a/azalea-client/src/plugins/mining.rs b/azalea-client/src/plugins/mining.rs
new file mode 100644
index 00000000..beb380b7
--- /dev/null
+++ b/azalea-client/src/plugins/mining.rs
@@ -0,0 +1,644 @@
+use azalea_block::{Block, BlockState, fluid_state::FluidState};
+use azalea_core::{direction::Direction, game_type::GameMode, position::BlockPos, tick::GameTick};
+use azalea_entity::{FluidOnEyes, Physics, mining::get_mine_progress};
+use azalea_inventory::ItemStack;
+use azalea_physics::PhysicsSet;
+use azalea_protocol::packets::game::s_player_action::{self, ServerboundPlayerAction};
+use azalea_world::{InstanceContainer, InstanceName};
+use bevy_app::{App, Plugin, Update};
+use bevy_ecs::prelude::*;
+use derive_more::{Deref, DerefMut};
+
+use crate::{
+ Client,
+ interact::{
+ CurrentSequenceNumber, HitResultComponent, SwingArmEvent, can_use_game_master_blocks,
+ check_is_interaction_restricted,
+ },
+ inventory::{Inventory, InventorySet},
+ local_player::{LocalGameMode, PermissionLevel, PlayerAbilities},
+ movement::MoveEventsSet,
+ packet::game::SendPacketEvent,
+};
+
+/// A plugin that allows clients to break blocks in the world.
+pub struct MiningPlugin;
+impl Plugin for MiningPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_event::<StartMiningBlockEvent>()
+ .add_event::<StartMiningBlockWithDirectionEvent>()
+ .add_event::<FinishMiningBlockEvent>()
+ .add_event::<StopMiningBlockEvent>()
+ .add_event::<MineBlockProgressEvent>()
+ .add_event::<AttackBlockEvent>()
+ .add_systems(
+ GameTick,
+ (continue_mining_block, handle_auto_mine)
+ .chain()
+ .before(PhysicsSet),
+ )
+ .add_systems(
+ Update,
+ (
+ handle_start_mining_block_event,
+ handle_start_mining_block_with_direction_event,
+ handle_finish_mining_block_event,
+ handle_stop_mining_block_event,
+ )
+ .chain()
+ .in_set(MiningSet)
+ .after(InventorySet)
+ .after(MoveEventsSet)
+ .before(azalea_entity::update_bounding_box)
+ .after(azalea_entity::update_fluid_on_eyes)
+ .after(crate::interact::update_hit_result_component)
+ .after(crate::attack::handle_attack_event)
+ .after(crate::interact::handle_block_interact_event)
+ .before(crate::interact::handle_swing_arm_event),
+ );
+ }
+}
+
+/// The Bevy system set for things related to mining.
+#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
+pub struct MiningSet;
+
+impl Client {
+ pub fn start_mining(&mut self, position: BlockPos) {
+ self.ecs.lock().send_event(StartMiningBlockEvent {
+ entity: self.entity,
+ position,
+ });
+ }
+
+ /// When enabled, the bot will mine any block that it is looking at if it is
+ /// reachable.
+ pub fn left_click_mine(&self, enabled: bool) {
+ let mut ecs = self.ecs.lock();
+ let mut entity_mut = ecs.entity_mut(self.entity);
+
+ if enabled {
+ entity_mut.insert(LeftClickMine);
+ } else {
+ entity_mut.remove::<LeftClickMine>();
+ }
+ }
+}
+
+/// A component that simulates the client holding down left click to mine the
+/// block that it's facing, but this only interacts with blocks and not
+/// entities.
+#[derive(Component)]
+pub struct LeftClickMine;
+
+#[allow(clippy::type_complexity)]
+fn handle_auto_mine(
+ mut query: Query<
+ (
+ &HitResultComponent,
+ Entity,
+ Option<&Mining>,
+ &Inventory,
+ &MineBlockPos,
+ &MineItem,
+ ),
+ With<LeftClickMine>,
+ >,
+ mut start_mining_block_event: EventWriter<StartMiningBlockEvent>,
+ mut stop_mining_block_event: EventWriter<StopMiningBlockEvent>,
+) {
+ for (
+ hit_result_component,
+ entity,
+ mining,
+ inventory,
+ current_mining_pos,
+ current_mining_item,
+ ) in &mut query.iter_mut()
+ {
+ let block_pos = hit_result_component.block_pos;
+
+ if (mining.is_none()
+ || !is_same_mining_target(
+ block_pos,
+ inventory,
+ current_mining_pos,
+ current_mining_item,
+ ))
+ && !hit_result_component.miss
+ {
+ start_mining_block_event.send(StartMiningBlockEvent {
+ entity,
+ position: block_pos,
+ });
+ } else if mining.is_some() && hit_result_component.miss {
+ stop_mining_block_event.send(StopMiningBlockEvent { entity });
+ }
+ }
+}
+
+/// Information about the block we're currently mining. This is only present if
+/// we're currently mining a block.
+#[derive(Component)]
+pub struct Mining {
+ pub pos: BlockPos,
+ pub dir: Direction,
+}
+
+/// Start mining the block at the given position.
+///
+/// If we're looking at the block then the correct direction will be used,
+/// otherwise it'll be [`Direction::Down`].
+#[derive(Event)]
+pub struct StartMiningBlockEvent {
+ pub entity: Entity,
+ pub position: BlockPos,
+}
+fn handle_start_mining_block_event(
+ mut events: EventReader<StartMiningBlockEvent>,
+ mut start_mining_events: EventWriter<StartMiningBlockWithDirectionEvent>,
+ mut query: Query<&HitResultComponent>,
+) {
+ for event in events.read() {
+ let hit_result = query.get_mut(event.entity).unwrap();
+ let direction = if hit_result.block_pos == event.position {
+ // we're looking at the block
+ hit_result.direction
+ } else {
+ // we're not looking at the block, arbitrary direction
+ Direction::Down
+ };
+ start_mining_events.send(StartMiningBlockWithDirectionEvent {
+ entity: event.entity,
+ position: event.position,
+ direction,
+ });
+ }
+}
+
+#[derive(Event)]
+pub struct StartMiningBlockWithDirectionEvent {
+ pub entity: Entity,
+ pub position: BlockPos,
+ pub direction: Direction,
+}
+#[allow(clippy::too_many_arguments, clippy::type_complexity)]
+fn handle_start_mining_block_with_direction_event(
+ mut events: EventReader<StartMiningBlockWithDirectionEvent>,
+ mut finish_mining_events: EventWriter<FinishMiningBlockEvent>,
+ mut send_packet_events: EventWriter<SendPacketEvent>,
+ mut attack_block_events: EventWriter<AttackBlockEvent>,
+ mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
+ mut query: Query<(
+ &InstanceName,
+ &LocalGameMode,
+ &Inventory,
+ &FluidOnEyes,
+ &Physics,
+ Option<&Mining>,
+ &mut CurrentSequenceNumber,
+ &mut MineDelay,
+ &mut MineProgress,
+ &mut MineTicks,
+ &mut MineItem,
+ &mut MineBlockPos,
+ )>,
+ instances: Res<InstanceContainer>,
+ mut commands: Commands,
+) {
+ for event in events.read() {
+ let (
+ instance_name,
+ game_mode,
+ inventory,
+ fluid_on_eyes,
+ physics,
+ mining,
+ mut sequence_number,
+ mut mine_delay,
+ mut mine_progress,
+ mut mine_ticks,
+ mut current_mining_item,
+ mut current_mining_pos,
+ ) = query.get_mut(event.entity).unwrap();
+
+ let instance_lock = instances.get(instance_name).unwrap();
+ let instance = instance_lock.read();
+ if check_is_interaction_restricted(
+ &instance,
+ &event.position,
+ &game_mode.current,
+ inventory,
+ ) {
+ continue;
+ }
+ // TODO (when world border is implemented): vanilla ignores if the block
+ // is outside of the worldborder
+
+ if game_mode.current == GameMode::Creative {
+ *sequence_number += 1;
+ finish_mining_events.send(FinishMiningBlockEvent {
+ entity: event.entity,
+ position: event.position,
+ });
+ **mine_delay = 5;
+ } else if mining.is_none()
+ || !is_same_mining_target(
+ event.position,
+ inventory,
+ &current_mining_pos,
+ &current_mining_item,
+ )
+ {
+ if mining.is_some() {
+ // send a packet to stop mining since we just changed target
+ send_packet_events.send(SendPacketEvent::new(
+ event.entity,
+ ServerboundPlayerAction {
+ action: s_player_action::Action::AbortDestroyBlock,
+ pos: current_mining_pos
+ .expect("IsMining is true so MineBlockPos must be present"),
+ direction: event.direction,
+ sequence: 0,
+ },
+ ));
+ }
+
+ let target_block_state = instance
+ .get_block_state(&event.position)
+ .unwrap_or_default();
+ *sequence_number += 1;
+ let target_registry_block = azalea_registry::Block::from(target_block_state);
+
+ // we can't break blocks if they don't have a bounding box
+
+ // TODO: So right now azalea doesn't differenciate between different types of
+ // bounding boxes. See ClipContext::block_shape for more info. Ideally this
+ // should just call ClipContext::block_shape and check if it's empty.
+ let block_is_solid = !target_block_state.is_air()
+ // this is a hack to make sure we can't break water or lava
+ && !matches!(
+ target_registry_block,
+ azalea_registry::Block::Water | azalea_registry::Block::Lava
+ );
+
+ if block_is_solid && **mine_progress == 0. {
+ // interact with the block (like note block left click) here
+ attack_block_events.send(AttackBlockEvent {
+ entity: event.entity,
+ position: event.position,
+ });
+ }
+
+ let block = Box::<dyn Block>::from(target_block_state);
+
+ let held_item = inventory.held_item();
+
+ if block_is_solid
+ && get_mine_progress(
+ block.as_ref(),
+ held_item.kind(),
+ &inventory.inventory_menu,
+ fluid_on_eyes,
+ physics,
+ ) >= 1.
+ {
+ // block was broken instantly
+ finish_mining_events.send(FinishMiningBlockEvent {
+ entity: event.entity,
+ position: event.position,
+ });
+ } else {
+ commands.entity(event.entity).insert(Mining {
+ pos: event.position,
+ dir: event.direction,
+ });
+ **current_mining_pos = Some(event.position);
+ **current_mining_item = held_item;
+ **mine_progress = 0.;
+ **mine_ticks = 0.;
+ mine_block_progress_events.send(MineBlockProgressEvent {
+ entity: event.entity,
+ position: event.position,
+ destroy_stage: mine_progress.destroy_stage(),
+ });
+ }
+
+ send_packet_events.send(SendPacketEvent::new(
+ event.entity,
+ ServerboundPlayerAction {
+ action: s_player_action::Action::StartDestroyBlock,
+ pos: event.position,
+ direction: event.direction,
+ sequence: **sequence_number,
+ },
+ ));
+ }
+ }
+}
+
+#[derive(Event)]
+pub struct MineBlockProgressEvent {
+ pub entity: Entity,
+ pub position: BlockPos,
+ pub destroy_stage: Option<u32>,
+}
+
+/// A player left clicked on a block, used for stuff like interacting with note
+/// blocks.
+#[derive(Event)]
+pub struct AttackBlockEvent {
+ pub entity: Entity,
+ pub position: BlockPos,
+}
+
+/// Returns whether the block and item are still the same as when we started
+/// mining.
+fn is_same_mining_target(
+ target_block: BlockPos,
+ inventory: &Inventory,
+ current_mining_pos: &MineBlockPos,
+ current_mining_item: &MineItem,
+) -> bool {
+ let held_item = inventory.held_item();
+ Some(target_block) == current_mining_pos.0 && held_item == current_mining_item.0
+}
+
+/// A component bundle for players that can mine blocks.
+#[derive(Bundle, Default)]
+pub struct MineBundle {
+ pub delay: MineDelay,
+ pub progress: MineProgress,
+ pub ticks: MineTicks,
+ pub mining_pos: MineBlockPos,
+ pub mine_item: MineItem,
+}
+
+/// A component that counts down until we start mining the next block.
+#[derive(Component, Debug, Default, Deref, DerefMut)]
+pub struct MineDelay(pub u32);
+
+/// A component that stores the progress of the current mining operation. This
+/// is a value between 0 and 1.
+#[derive(Component, Debug, Default, Deref, DerefMut)]
+pub struct MineProgress(pub f32);
+
+impl MineProgress {
+ pub fn destroy_stage(&self) -> Option<u32> {
+ if self.0 > 0. {
+ Some((self.0 * 10.) as u32)
+ } else {
+ None
+ }
+ }
+}
+
+/// A component that stores the number of ticks that we've been mining the same
+/// block for. This is a float even though it should only ever be a round
+/// number.
+#[derive(Component, Clone, Debug, Default, Deref, DerefMut)]
+pub struct MineTicks(pub f32);
+
+/// A component that stores the position of the block we're currently mining.
+#[derive(Component, Clone, Debug, Default, Deref, DerefMut)]
+pub struct MineBlockPos(pub Option<BlockPos>);
+
+/// A component that contains the item we're currently using to mine. If we're
+/// not mining anything, it'll be [`ItemStack::Empty`].
+#[derive(Component, Clone, Debug, Default, Deref, DerefMut)]
+pub struct MineItem(pub ItemStack);
+
+/// Sent when we completed mining a block.
+#[derive(Event)]
+pub struct FinishMiningBlockEvent {
+ pub entity: Entity,
+ pub position: BlockPos,
+}
+
+pub fn handle_finish_mining_block_event(
+ mut events: EventReader<FinishMiningBlockEvent>,
+ mut query: Query<(
+ &InstanceName,
+ &LocalGameMode,
+ &Inventory,
+ &PlayerAbilities,
+ &PermissionLevel,
+ &mut CurrentSequenceNumber,
+ )>,
+ instances: Res<InstanceContainer>,
+) {
+ for event in events.read() {
+ let (instance_name, game_mode, inventory, abilities, permission_level, _sequence_number) =
+ query.get_mut(event.entity).unwrap();
+ let instance_lock = instances.get(instance_name).unwrap();
+ let instance = instance_lock.read();
+ if check_is_interaction_restricted(
+ &instance,
+ &event.position,
+ &game_mode.current,
+ inventory,
+ ) {
+ continue;
+ }
+
+ if game_mode.current == GameMode::Creative {
+ let held_item = inventory.held_item().kind();
+ if matches!(
+ held_item,
+ azalea_registry::Item::Trident | azalea_registry::Item::DebugStick
+ ) || azalea_registry::tags::items::SWORDS.contains(&held_item)
+ {
+ continue;
+ }
+ }
+
+ let Some(block_state) = instance.get_block_state(&event.position) else {
+ continue;
+ };
+
+ let registry_block = Box::<dyn Block>::from(block_state).as_registry_block();
+ if !can_use_game_master_blocks(abilities, permission_level)
+ && matches!(
+ registry_block,
+ azalea_registry::Block::CommandBlock | azalea_registry::Block::StructureBlock
+ )
+ {
+ continue;
+ }
+ if block_state == BlockState::AIR {
+ continue;
+ }
+
+ // when we break a waterlogged block we want to keep the water there
+ let fluid_state = FluidState::from(block_state);
+ let block_state_for_fluid = BlockState::from(fluid_state);
+ instance.set_block_state(&event.position, block_state_for_fluid);
+ }
+}
+
+/// Abort mining a block.
+#[derive(Event)]
+pub struct StopMiningBlockEvent {
+ pub entity: Entity,
+}
+pub fn handle_stop_mining_block_event(
+ mut events: EventReader<StopMiningBlockEvent>,
+ mut send_packet_events: EventWriter<SendPacketEvent>,
+ mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
+ mut query: Query<(&mut Mining, &MineBlockPos, &mut MineProgress)>,
+ mut commands: Commands,
+) {
+ for event in events.read() {
+ let (mut _mining, mine_block_pos, mut mine_progress) = query.get_mut(event.entity).unwrap();
+
+ let mine_block_pos =
+ mine_block_pos.expect("IsMining is true so MineBlockPos must be present");
+ send_packet_events.send(SendPacketEvent::new(
+ event.entity,
+ ServerboundPlayerAction {
+ action: s_player_action::Action::AbortDestroyBlock,
+ pos: mine_block_pos,
+ direction: Direction::Down,
+ sequence: 0,
+ },
+ ));
+ commands.entity(event.entity).remove::<Mining>();
+ **mine_progress = 0.;
+ mine_block_progress_events.send(MineBlockProgressEvent {
+ entity: event.entity,
+ position: mine_block_pos,
+ destroy_stage: None,
+ });
+ }
+}
+
+#[allow(clippy::too_many_arguments, clippy::type_complexity)]
+pub fn continue_mining_block(
+ mut query: Query<(
+ Entity,
+ &InstanceName,
+ &LocalGameMode,
+ &Inventory,
+ &MineBlockPos,
+ &MineItem,
+ &FluidOnEyes,
+ &Physics,
+ &Mining,
+ &mut MineDelay,
+ &mut MineProgress,
+ &mut MineTicks,
+ &mut CurrentSequenceNumber,
+ )>,
+ mut send_packet_events: EventWriter<SendPacketEvent>,
+ mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
+ mut finish_mining_events: EventWriter<FinishMiningBlockEvent>,
+ mut start_mining_events: EventWriter<StartMiningBlockWithDirectionEvent>,
+ mut swing_arm_events: EventWriter<SwingArmEvent>,
+ instances: Res<InstanceContainer>,
+ mut commands: Commands,
+) {
+ for (
+ entity,
+ instance_name,
+ game_mode,
+ inventory,
+ current_mining_pos,
+ current_mining_item,
+ fluid_on_eyes,
+ physics,
+ mining,
+ mut mine_delay,
+ mut mine_progress,
+ mut mine_ticks,
+ mut sequence_number,
+ ) in query.iter_mut()
+ {
+ if **mine_delay > 0 {
+ **mine_delay -= 1;
+ continue;
+ }
+
+ if game_mode.current == GameMode::Creative {
+ // TODO: worldborder check
+ **mine_delay = 5;
+ finish_mining_events.send(FinishMiningBlockEvent {
+ entity,
+ position: mining.pos,
+ });
+ *sequence_number += 1;
+ send_packet_events.send(SendPacketEvent::new(
+ entity,
+ ServerboundPlayerAction {
+ action: s_player_action::Action::StartDestroyBlock,
+ pos: mining.pos,
+ direction: mining.dir,
+ sequence: **sequence_number,
+ },
+ ));
+ swing_arm_events.send(SwingArmEvent { entity });
+ } else if is_same_mining_target(
+ mining.pos,
+ inventory,
+ current_mining_pos,
+ current_mining_item,
+ ) {
+ let instance_lock = instances.get(instance_name).unwrap();
+ let instance = instance_lock.read();
+ let target_block_state = instance.get_block_state(&mining.pos).unwrap_or_default();
+
+ if target_block_state.is_air() {
+ commands.entity(entity).remove::<Mining>();
+ continue;
+ }
+ let block = Box::<dyn Block>::from(target_block_state);
+ **mine_progress += get_mine_progress(
+ block.as_ref(),
+ current_mining_item.kind(),
+ &inventory.inventory_menu,
+ fluid_on_eyes,
+ physics,
+ );
+
+ if **mine_ticks % 4. == 0. {
+ // vanilla makes a mining sound here
+ }
+ **mine_ticks += 1.;
+
+ if **mine_progress >= 1. {
+ commands.entity(entity).remove::<Mining>();
+ *sequence_number += 1;
+ finish_mining_events.send(FinishMiningBlockEvent {
+ entity,
+ position: mining.pos,
+ });
+ send_packet_events.send(SendPacketEvent::new(
+ entity,
+ ServerboundPlayerAction {
+ action: s_player_action::Action::StopDestroyBlock,
+ pos: mining.pos,
+ direction: mining.dir,
+ sequence: **sequence_number,
+ },
+ ));
+ **mine_progress = 0.;
+ **mine_ticks = 0.;
+ **mine_delay = 0;
+ }
+
+ mine_block_progress_events.send(MineBlockProgressEvent {
+ entity,
+ position: mining.pos,
+ destroy_stage: mine_progress.destroy_stage(),
+ });
+ swing_arm_events.send(SwingArmEvent { entity });
+ } else {
+ start_mining_events.send(StartMiningBlockWithDirectionEvent {
+ entity,
+ position: mining.pos,
+ direction: mining.dir,
+ });
+ }
+
+ swing_arm_events.send(SwingArmEvent { entity });
+ }
+}