diff options
| author | mat <27899617+mat-1@users.noreply.github.com> | 2023-07-14 22:20:40 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-07-14 22:20:40 -0500 |
| commit | 7405427199e5a994d4a6a706f84434a69cb7a7d9 (patch) | |
| tree | ca537e5d761bc053187d952fced0915c850b92aa /azalea-client/src/mining.rs | |
| parent | d1afd02aa84e7b4450c1607277f078eb2a0f1bf3 (diff) | |
| download | azalea-drasl-7405427199e5a994d4a6a706f84434a69cb7a7d9.tar.xz | |
Mining (#95)
* more mining stuff
* initialize azalea-tags crate
* more mining stuff 2
* mining in ecs
* well technically mining works but
no codegen for how long it takes to mine each block yet
* rename downloads to __cache__
it was bothering me since it's not *just* downloads
* codegen block behavior
* fix not sending packet to finish breaking block
* mining animation 🎉
* clippy
* cleanup, move Client::mine into a client extension
* add azalea/src/mining.rs
---------
Co-authored-by: mat <git@matdoes.dev>
Diffstat (limited to 'azalea-client/src/mining.rs')
| -rw-r--r-- | azalea-client/src/mining.rs | 537 |
1 files changed, 522 insertions, 15 deletions
diff --git a/azalea-client/src/mining.rs b/azalea-client/src/mining.rs index 241c4fde..049bc859 100644 --- a/azalea-client/src/mining.rs +++ b/azalea-client/src/mining.rs @@ -1,36 +1,543 @@ -use azalea_core::BlockPos; -use bevy_app::{App, Plugin, Update}; +use azalea_block::{Block, BlockState, FluidState}; +use azalea_core::{BlockPos, Direction, GameMode}; +use azalea_entity::{mining::get_mine_progress, FluidOnEyes, Physics}; +use azalea_inventory::ItemSlot; +use azalea_protocol::packets::game::serverbound_player_action_packet::{ + self, ServerboundPlayerActionPacket, +}; +use azalea_world::{InstanceContainer, InstanceName}; +use bevy_app::{App, FixedUpdate, Plugin, Update}; use bevy_ecs::prelude::*; +use derive_more::{Deref, DerefMut}; -use crate::Client; +use crate::{ + client::{PermissionLevel, PlayerAbilities}, + interact::{ + can_use_game_master_blocks, check_is_interaction_restricted, CurrentSequenceNumber, + HitResultComponent, SwingArmEvent, + }, + inventory::InventoryComponent, + local_player::{LocalGameMode, SendPacketEvent}, +}; /// 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::<StartMiningBlockEvent>() - .add_systems(Update, handle_start_mining_block_event); + .add_event::<StartMiningBlockWithDirectionEvent>() + .add_event::<FinishMiningBlockEvent>() + .add_event::<StopMiningBlockEvent>() + .add_event::<MineBlockProgressEvent>() + .add_event::<AttackBlockEvent>() + .add_systems(FixedUpdate, continue_mining_block) + .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(), + ); } } -impl Client { - /// Start mining a block. - pub fn start_mining_block(&self, position: BlockPos) { - self.ecs.lock().send_event(StartMiningBlockEvent { - entity: self.entity, - position, +/// 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.iter() { + 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 StartMiningBlockEvent { +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, + &InventoryComponent, + &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.iter() { + 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 { + entity: event.entity, + packet: ServerboundPlayerActionPacket { + action: serverbound_player_action_packet::Action::AbortDestroyBlock, + pos: current_mining_pos + .expect("IsMining is true so MineBlockPos must be present"), + direction: event.direction, + sequence: 0, + } + .get(), + }); + } + + let target_block_state = instance + .get_block_state(&event.position) + .unwrap_or_default(); + *sequence_number += 1; + let block_is_solid = !target_block_state.is_air(); + 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 { + entity: event.entity, + packet: ServerboundPlayerActionPacket { + action: serverbound_player_action_packet::Action::StartDestroyBlock, + pos: event.position, + direction: event.direction, + sequence: **sequence_number, + } + .get(), + }); + } + } +} + +#[derive(Event)] +pub struct MineBlockProgressEvent { pub entity: Entity, pub position: BlockPos, + pub destroy_stage: Option<u32>, } -fn handle_start_mining_block_event(mut _events: EventReader<StartMiningBlockEvent>) { - // for event in events.iter() { - // // - // } +/// 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: &InventoryComponent, + 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, Debug, Default, Deref, DerefMut)] +pub struct MineTicks(pub f32); + +/// A component that stores the position of the block we're currently mining. +#[derive(Component, 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 [`ItemSlot::Empty`]. +#[derive(Component, Debug, Default, Deref, DerefMut)] +pub struct MineItem(pub ItemSlot); + +/// Sent when we completed mining a block. +#[derive(Event)] +pub struct FinishMiningBlockEvent { + pub entity: Entity, + pub position: BlockPos, +} + +fn handle_finish_mining_block_event( + mut events: EventReader<FinishMiningBlockEvent>, + mut query: Query<( + &InstanceName, + &LocalGameMode, + &InventoryComponent, + &PlayerAbilities, + &PermissionLevel, + &mut CurrentSequenceNumber, + )>, + instances: Res<InstanceContainer>, +) { + for event in events.iter() { + 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, +} +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.iter() { + 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 { + entity: event.entity, + packet: ServerboundPlayerActionPacket { + action: serverbound_player_action_packet::Action::AbortDestroyBlock, + pos: mine_block_pos, + direction: Direction::Down, + sequence: 0, + } + .get(), + }); + 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)] +fn continue_mining_block( + mut query: Query<( + Entity, + &InstanceName, + &LocalGameMode, + &InventoryComponent, + &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 { + entity, + packet: ServerboundPlayerActionPacket { + action: serverbound_player_action_packet::Action::StartDestroyBlock, + pos: mining.pos, + direction: mining.dir, + sequence: **sequence_number, + } + .get(), + }); + 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 { + entity, + packet: ServerboundPlayerActionPacket { + action: serverbound_player_action_packet::Action::StopDestroyBlock, + pos: mining.pos, + direction: mining.dir, + sequence: **sequence_number, + } + .get(), + }); + **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 }); + } } |
