diff options
Diffstat (limited to 'azalea-client/src/plugins/mining.rs')
| -rw-r--r-- | azalea-client/src/plugins/mining.rs | 644 |
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, + ¤t_mining_pos, + ¤t_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 }); + } +} |
