use azalea_block::{Block, BlockState, FluidState}; use azalea_core::{direction::Direction, game_type::GameMode, position::BlockPos, tick::GameTick}; use azalea_entity::{mining::get_mine_progress, FluidOnEyes, Physics}; 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::{ interact::{ can_use_game_master_blocks, check_is_interaction_restricted, CurrentSequenceNumber, HitResultComponent, SwingArmEvent, }, inventory::{Inventory, InventorySet}, local_player::{LocalGameMode, PermissionLevel, PlayerAbilities}, movement::MoveEventsSet, packet_handling::game::SendPacketEvent, Client, }; /// A plugin that allows clients to break blocks in the world. pub struct MinePlugin; impl Plugin for MinePlugin { fn build(&self, app: &mut App) { app.add_event::() .add_event::() .add_event::() .add_event::() .add_event::() .add_event::() .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), ); } } #[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::(); } } } /// 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: EventWriter, mut stop_mining_block_event: EventWriter, ) { 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, mut start_mining_events: EventWriter, 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, mut finish_mining_events: EventWriter, mut send_packet_events: EventWriter, mut attack_block_events: EventWriter, mut mine_block_progress_events: EventWriter, mut query: Query<( &InstanceName, &LocalGameMode, &Inventory, &FluidOnEyes, &Physics, Option<&Mining>, &mut CurrentSequenceNumber, &mut MineDelay, &mut MineProgress, &mut MineTicks, &mut MineItem, &mut MineBlockPos, )>, instances: Res, 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::::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, } /// 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 { 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); /// 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, mut query: Query<( &InstanceName, &LocalGameMode, &Inventory, &PlayerAbilities, &PermissionLevel, &mut CurrentSequenceNumber, )>, instances: Res, ) { 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::::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, mut send_packet_events: EventWriter, mut mine_block_progress_events: EventWriter, 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::(); **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, mut mine_block_progress_events: EventWriter, mut finish_mining_events: EventWriter, mut start_mining_events: EventWriter, mut swing_arm_events: EventWriter, instances: Res, 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::(); continue; } let block = Box::::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::(); *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 }); } }