use azalea_block::{BlockState, BlockTrait, fluid_state::FluidState}; use azalea_core::{direction::Direction, game_type::GameMode, position::BlockPos, tick::GameTick}; use azalea_entity::{ ActiveEffects, Attributes, FluidOnEyes, Physics, PlayerAbilities, Position, inventory::Inventory, mining::get_mine_progress, }; use azalea_inventory::ItemStack; use azalea_physics::{PhysicsSystems, collision::BlockWithShape}; use azalea_protocol::packets::game::s_player_action::{self, ServerboundPlayerAction}; use azalea_registry::builtin::{BlockKind, ItemKind}; use azalea_world::{WorldName, Worlds}; use bevy_app::{App, Plugin, Update}; use bevy_ecs::prelude::*; use derive_more::{Deref, DerefMut}; use tracing::{debug, trace, warn}; use crate::{ interact::{ BlockStatePredictionHandler, SwingArmEvent, can_use_game_master_blocks, check_is_interaction_restricted, pick::HitResultComponent, }, inventory::InventorySystems, local_player::{LocalGameMode, PermissionLevel, WorldHolder}, movement::MoveEventsSystems, packet::game::SendGamePacketEvent, }; /// 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_message::() .add_message::() .add_message::() .add_message::() .add_systems( GameTick, ( update_mining_component, handle_auto_mine, handle_mining_queued, decrement_mine_delay, continue_mining_block, ) .chain() .before(PhysicsSystems) .before(super::movement::send_position) .before(super::interact::handle_start_use_item_queued) .after(azalea_entity::update_fluid_on_eyes) .in_set(MiningSystems), ) .add_systems( Update, ( handle_start_mining_block_event, handle_stop_mining_block_event, ) .chain() .in_set(MiningSystems) .after(InventorySystems) .after(MoveEventsSystems) .after(crate::interact::pick::update_hit_result_component) .after(crate::attack::handle_attack_event), ) .add_observer(handle_finish_mining_block_observer); } } /// The Bevy system set for things related to mining. #[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)] pub struct MiningSystems; /// 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, >, mut start_mining_block_event: MessageWriter, mut stop_mining_block_event: MessageWriter, ) { for ( hit_result_component, entity, mining, inventory, current_mining_pos, current_mining_item, ) in &mut query.iter_mut() { let block_pos = hit_result_component .as_block_hit_result_if_not_miss() .map(|b| b.block_pos); // start mining if we're looking at a block and we're not already mining it if let Some(block_pos) = block_pos && (mining.is_none() || !is_same_mining_target( block_pos, inventory, current_mining_pos, current_mining_item, )) { start_mining_block_event.write(StartMiningBlockEvent { entity, position: block_pos, force: true, }); } else if mining.is_some() && hit_result_component.miss() { stop_mining_block_event.write(StopMiningBlockEvent { entity }); } } } /// Information about the block we're currently mining. /// /// This is only present if we're currently mining a block. #[derive(Clone, Component, Debug)] pub struct Mining { pub pos: BlockPos, pub dir: Direction, /// See [`MiningQueued::force`]. pub force: bool, } /// 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(Debug, Message)] pub struct StartMiningBlockEvent { pub entity: Entity, pub position: BlockPos, /// Whether we should ignore blocks that are blocking the view of this /// block. /// /// Most of the time, you'll want to set this to true as it'll make the /// behavior more predictable. If it's set to false, then it might fail or /// it might mine blocks other than the one at `position` (which may be /// preferable if you're trying to act like vanilla). pub force: bool, } fn handle_start_mining_block_event( mut commands: Commands, mut events: MessageReader, mut query: Query<&HitResultComponent>, ) { for event in events.read() { trace!("{event:?}"); let hit_result = query.get_mut(event.entity).unwrap(); if event.force { let direction = if let Some(block_hit_result) = hit_result.as_block_hit_result_if_not_miss() && block_hit_result.block_pos == event.position { // we're looking at the block block_hit_result.direction } else { debug!( "Got StartMiningBlockEvent but we're not looking at the block ({hit_result:?}.block_pos != {:?}). Picking an arbitrary direction instead.", event.position ); // we're not looking at the block, arbitrary direction Direction::Down }; commands.entity(event.entity).insert(MiningQueued { position: event.position, direction, force: true, }); } else { // let block_hit_result = hit_result.as_block_hit_result_if_not_miss(); // let direction = block_hit_result.map_or(Direction::Down, |b| b.direction); if let Some(block_hit_result) = hit_result.as_block_hit_result_if_not_miss() && block_hit_result.block_pos == event.position { commands.entity(event.entity).insert(MiningQueued { position: event.position, direction: block_hit_result.direction, force: false, }); } else { warn!( "Got StartMiningBlockEvent with force=false but we're not looking at the block ({hit_result:?}.block_pos != {:?}). You should've looked at the block before trying to mine with force=false.", event.position ); }; } } } /// Present on entities when they're going to start mining a block next tick. #[derive(Clone, Component, Debug)] pub struct MiningQueued { pub position: BlockPos, pub direction: Direction, /// Whether we should mine the block regardless of whether it's reachable. pub force: bool, } #[allow(clippy::too_many_arguments, clippy::type_complexity)] pub fn handle_mining_queued( mut commands: Commands, mut attack_block_events: MessageWriter, mut mine_block_progress_events: MessageWriter, query: Query<( Entity, &MiningQueued, &WorldHolder, &LocalGameMode, &Inventory, &ActiveEffects, &FluidOnEyes, &Physics, &Attributes, Option<&mut Mining>, &mut BlockStatePredictionHandler, ( &mut MineDelay, &mut MineProgress, &mut MineTicks, &mut MineItem, &mut MineBlockPos, ), )>, ) { for ( entity, mining_queued, world_holder, game_mode, inventory, active_effects, fluid_on_eyes, physics, attributes, mut mining, mut sequence_number, ( mut mine_delay, mut mine_progress, mut mine_ticks, mut current_mining_item, mut current_mining_pos, ), ) in query { trace!("handle_mining_queued {mining_queued:?}"); commands.entity(entity).remove::(); let world = world_holder.shared.read(); if check_is_interaction_restricted( &world, mining_queued.position, &game_mode.current, inventory, ) { continue; } // TODO (when world border is implemented): vanilla ignores if the block // is outside of the worldborder if let Some(mining) = &mut mining { // this matters if we were previously mining a block without force if mining_queued.force { mining.force = true; } } if game_mode.current == GameMode::Creative { // In creative mode, first send START_DESTROY_BLOCK packet then immediately // finish mining commands.trigger(SendGamePacketEvent::new( entity, ServerboundPlayerAction { action: s_player_action::Action::StartDestroyBlock, pos: mining_queued.position, direction: mining_queued.direction, seq: sequence_number.start_predicting(), }, )); commands.trigger(FinishMiningBlockEvent { entity, position: mining_queued.position, }); **mine_delay = 5; commands.trigger(SwingArmEvent { entity }); } else if mining.is_none() || !is_same_mining_target( mining_queued.position, inventory, ¤t_mining_pos, ¤t_mining_item, ) { if mining.is_some() { // send a packet to stop mining since we just changed target commands.trigger(SendGamePacketEvent::new( entity, ServerboundPlayerAction { action: s_player_action::Action::AbortDestroyBlock, pos: current_mining_pos .expect("IsMining is true so MineBlockPos must be present"), direction: mining_queued.direction, seq: 0, }, )); } let target_block_state = world .get_block_state(mining_queued.position) .unwrap_or_default(); // we can't break blocks if they don't have a bounding box let block_is_solid = !target_block_state.outline_shape().is_empty(); if block_is_solid && **mine_progress == 0. { // interact with the block (like note block left click) here attack_block_events.write(AttackBlockEvent { entity, position: mining_queued.position, }); } let block = Box::::from(target_block_state); let held_item = inventory.held_item(); if block_is_solid && get_mine_progress( block.as_ref(), held_item, fluid_on_eyes, physics, attributes, active_effects, ) >= 1. { // block was broken instantly (instamined) commands.trigger(FinishMiningBlockEvent { entity, position: mining_queued.position, }); } else { let mining = Mining { pos: mining_queued.position, dir: mining_queued.direction, force: mining_queued.force, }; trace!("inserting mining component {mining:?} for entity {entity:?}"); commands.entity(entity).insert(mining); **current_mining_pos = Some(mining_queued.position); **current_mining_item = held_item.clone(); **mine_progress = 0.; **mine_ticks = 0.; mine_block_progress_events.write(MineBlockProgressEvent { entity, position: mining_queued.position, destroy_stage: mine_progress.destroy_stage(), }); } commands.trigger(SendGamePacketEvent::new( entity, ServerboundPlayerAction { action: s_player_action::Action::StartDestroyBlock, pos: mining_queued.position, direction: mining_queued.direction, seq: sequence_number.start_predicting(), }, )); commands.trigger(SwingArmEvent { entity }); // another swing packet gets sent in the same tick in // continue_mining_block, vanilla does this too } } } #[derive(Message)] pub struct MineBlockProgressEvent { pub entity: Entity, pub position: BlockPos, pub destroy_stage: Option, } /// A player left clicked on a block, used for stuff like interacting with note /// blocks. #[derive(Message)] 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 == ¤t_mining_item.0 } /// A component bundle for players that can mine blocks. #[derive(Bundle, Clone, 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(Clone, 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(Clone, Component, Debug, Default, Deref, DerefMut)] pub struct MineProgress(pub f32); impl MineProgress { pub fn destroy_stage(&self) -> Option { 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 despite the fact that it should only ever be a round number. #[derive(Clone, Component, Debug, Default, Deref, DerefMut)] pub struct MineTicks(pub f32); /// A component that stores the position of the block we're currently mining. #[derive(Clone, Component, Debug, Default, Deref, DerefMut)] pub struct MineBlockPos(pub Option); /// A component that contains the item we're currently using to mine, or /// [`ItemStack::Empty`] if nothing is being mined. #[derive(Clone, Component, Debug, Default, Deref, DerefMut)] pub struct MineItem(pub ItemStack); /// A trigger that's sent when we completed mining a block. #[derive(EntityEvent)] pub struct FinishMiningBlockEvent { pub entity: Entity, pub position: BlockPos, } pub fn handle_finish_mining_block_observer( finish_mining_block: On, mut query: Query<( &WorldName, &LocalGameMode, &Inventory, &PlayerAbilities, &PermissionLevel, &Position, &mut BlockStatePredictionHandler, )>, worlds: Res, ) { let event = finish_mining_block.event(); let ( world_name, game_mode, inventory, abilities, permission_level, player_pos, mut prediction_handler, ) = query.get_mut(finish_mining_block.entity).unwrap(); let world_lock = worlds.get(world_name).unwrap(); let world = world_lock.read(); if check_is_interaction_restricted(&world, event.position, &game_mode.current, inventory) { return; } if game_mode.current == GameMode::Creative { let held_item = inventory.held_item().kind(); if matches!(held_item, ItemKind::Trident | ItemKind::DebugStick) || azalea_registry::tags::items::SWORDS.contains(&held_item) { return; } } let Some(block_state) = world.get_block_state(event.position) else { return; }; let registry_block = block_state.as_block_kind(); if !can_use_game_master_blocks(abilities, permission_level) && matches!( registry_block, BlockKind::CommandBlock | BlockKind::StructureBlock ) { return; } if block_state == BlockState::AIR { return; } // 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); let old_state = world .set_block_state(event.position, block_state_for_fluid) .unwrap_or_default(); prediction_handler.retain_known_server_state(event.position, old_state, **player_pos); } /// Abort mining a block. #[derive(Message)] pub struct StopMiningBlockEvent { pub entity: Entity, } pub fn handle_stop_mining_block_event( mut events: MessageReader, mut commands: Commands, mut mine_block_progress_events: MessageWriter, mut query: Query<(&MineBlockPos, &mut MineProgress)>, ) { for event in events.read() { let (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"); commands.trigger(SendGamePacketEvent::new( event.entity, ServerboundPlayerAction { action: s_player_action::Action::AbortDestroyBlock, pos: mine_block_pos, direction: Direction::Down, seq: 0, }, )); commands.entity(event.entity).remove::(); **mine_progress = 0.; mine_block_progress_events.write(MineBlockProgressEvent { entity: event.entity, position: mine_block_pos, destroy_stage: None, }); } } pub fn decrement_mine_delay(mut query: Query<&mut MineDelay>) { for mut mine_delay in &mut query { if **mine_delay > 0 { **mine_delay -= 1; } } } #[allow(clippy::too_many_arguments, clippy::type_complexity)] pub fn continue_mining_block( mut query: Query<( Entity, &WorldName, &LocalGameMode, &Inventory, &MineBlockPos, &MineItem, &ActiveEffects, &FluidOnEyes, &Physics, &Attributes, &Mining, &mut MineDelay, &mut MineProgress, &mut MineTicks, &mut BlockStatePredictionHandler, )>, mut commands: Commands, mut mine_block_progress_events: MessageWriter, worlds: Res, ) { for ( entity, world_name, game_mode, inventory, current_mining_pos, current_mining_item, active_effects, fluid_on_eyes, physics, attributes, mining, mut mine_delay, mut mine_progress, mut mine_ticks, mut prediction_handler, ) in query.iter_mut() { if game_mode.current == GameMode::Creative { // TODO: worldborder check **mine_delay = 5; commands.trigger(SendGamePacketEvent::new( entity, ServerboundPlayerAction { action: s_player_action::Action::StartDestroyBlock, pos: mining.pos, direction: mining.dir, seq: prediction_handler.start_predicting(), }, )); commands.trigger(FinishMiningBlockEvent { entity, position: mining.pos, }); commands.trigger(SwingArmEvent { entity }); } else if mining.force || is_same_mining_target( mining.pos, inventory, current_mining_pos, current_mining_item, ) { trace!("continue mining block at {:?}", mining.pos); let world_lock = worlds.get(world_name).unwrap(); let world = world_lock.read(); let target_block_state = world.get_block_state(mining.pos).unwrap_or_default(); trace!("target_block_state: {target_block_state:?}"); if target_block_state.is_air() { commands.entity(entity).remove::(); continue; } let block = Box::::from(target_block_state); **mine_progress += get_mine_progress( block.as_ref(), current_mining_item, fluid_on_eyes, physics, attributes, active_effects, ); if **mine_ticks % 4. == 0. { // vanilla makes a mining sound here } **mine_ticks += 1.; if **mine_progress >= 1. { // MiningQueued is removed in case we were doing an infinite loop that // repeatedly inserts MiningQueued commands.entity(entity).remove::<(Mining, MiningQueued)>(); trace!("finished mining block at {:?}", mining.pos); commands.trigger(FinishMiningBlockEvent { entity, position: mining.pos, }); commands.trigger(SendGamePacketEvent::new( entity, ServerboundPlayerAction { action: s_player_action::Action::StopDestroyBlock, pos: mining.pos, direction: mining.dir, seq: prediction_handler.start_predicting(), }, )); **mine_progress = 0.; **mine_ticks = 0.; **mine_delay = 5; } mine_block_progress_events.write(MineBlockProgressEvent { entity, position: mining.pos, destroy_stage: mine_progress.destroy_stage(), }); commands.trigger(SwingArmEvent { entity }); } else { trace!("switching mining target to {:?}", mining.pos); commands.entity(entity).insert(MiningQueued { position: mining.pos, direction: mining.dir, force: false, }); } } } pub fn update_mining_component( mut commands: Commands, mut query: Query<(Entity, &mut Mining, &HitResultComponent)>, ) { for (entity, mut mining, hit_result_component) in &mut query.iter_mut() { if let Some(block_hit_result) = hit_result_component.as_block_hit_result_if_not_miss() { if mining.force && block_hit_result.block_pos != mining.pos { continue; } if mining.pos != block_hit_result.block_pos { debug!( "Updating Mining::pos from {:?} to {:?}", mining.pos, block_hit_result.block_pos ); mining.pos = block_hit_result.block_pos; } mining.dir = block_hit_result.direction; } else { if mining.force { continue; } debug!("Removing mining component because we're no longer looking at the block"); commands.entity(entity).remove::(); } } }