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 | |
| 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')
| -rwxr-xr-x | azalea-client/src/account.rs | 5 | ||||
| -rw-r--r-- | azalea-client/src/client.rs | 28 | ||||
| -rw-r--r-- | azalea-client/src/entity_query.rs | 6 | ||||
| -rw-r--r-- | azalea-client/src/events.rs | 2 | ||||
| -rw-r--r-- | azalea-client/src/interact.rs | 88 | ||||
| -rw-r--r-- | azalea-client/src/inventory.rs | 25 | ||||
| -rw-r--r-- | azalea-client/src/lib.rs | 2 | ||||
| -rw-r--r-- | azalea-client/src/local_player.rs | 10 | ||||
| -rw-r--r-- | azalea-client/src/mining.rs | 537 | ||||
| -rw-r--r-- | azalea-client/src/movement.rs | 31 | ||||
| -rw-r--r-- | azalea-client/src/packet_handling.rs | 41 | ||||
| -rwxr-xr-x | azalea-client/src/player.rs | 2 |
12 files changed, 669 insertions, 108 deletions
diff --git a/azalea-client/src/account.rs b/azalea-client/src/account.rs index 2d1b766c..0e67a79a 100755 --- a/azalea-client/src/account.rs +++ b/azalea-client/src/account.rs @@ -187,7 +187,10 @@ impl Account { *access_token_mutex.lock() = new_access_token; let AccountOpts::MicrosoftWithAccessToken { msa: new_msa } = - new_account.account_opts else { unreachable!() }; + new_account.account_opts + else { + unreachable!() + }; *msa.lock() = new_msa.lock().clone(); Ok(()) diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index 725cd9f3..0b1fccc1 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -8,6 +8,7 @@ use crate::{ death_event, handle_send_packet_event, update_in_loaded_chunk, GameProfileComponent, LocalPlayer, PhysicsState, SendPacketEvent, }, + mining::{self, MinePlugin}, movement::{LastSentLookDirection, PlayerMovePlugin}, packet_handling::{self, PacketHandlerPlugin, PacketReceiver}, player::retroactively_add_game_profile_component, @@ -19,6 +20,7 @@ use crate::{ use azalea_auth::{game_profile::GameProfile, sessionserver::ClientSessionServerError}; use azalea_chat::FormattedText; use azalea_core::Vec3; +use azalea_entity::{EntityPlugin, EntityUpdateSet, Local, Position}; use azalea_physics::{PhysicsPlugin, PhysicsSet}; use azalea_protocol::{ connect::{Connection, ConnectionError}, @@ -41,10 +43,7 @@ use azalea_protocol::{ }, resolver, ServerAddress, }; -use azalea_world::{ - entity::{EntityPlugin, EntityUpdateSet, Local, Position, WorldName}, - Instance, InstanceContainer, PartialInstance, -}; +use azalea_world::{Instance, InstanceContainer, InstanceName, PartialInstance}; use bevy_app::{App, FixedUpdate, Main, Plugin, PluginGroup, PluginGroupBuilder, Update}; use bevy_ecs::{ bundle::Bundle, @@ -132,6 +131,10 @@ impl From<ClientboundPlayerAbilitiesPacket> for PlayerAbilities { } } +/// Level must be 0..=4 +#[derive(Component, Clone, Default, Deref, DerefMut)] +pub struct PermissionLevel(pub u8); + /// A component that contains a map of player UUIDs to their information in the /// tab list. /// @@ -301,6 +304,8 @@ impl Client { current_sequence_number: CurrentSequenceNumber::default(), last_sent_direction: LastSentLookDirection::default(), abilities: PlayerAbilities::default(), + permission_level: PermissionLevel::default(), + mining: mining::MineBundle::default(), _local: Local, }); @@ -466,9 +471,9 @@ impl Client { /// # Examples /// /// ``` - /// # use azalea_world::entity::WorldName; + /// # use azalea_world::InstanceName; /// # fn example(client: &azalea_client::Client) { - /// let world_name = client.component::<WorldName>(); + /// let world_name = client.component::<InstanceName>(); /// # } pub fn component<T: Component + Clone>(&self) -> T { self.query::<&T>(&mut self.ecs.lock()).clone() @@ -486,7 +491,7 @@ impl Client { /// If the client using a shared world, then the shared world will be a /// superset of the client's world. pub fn world(&self) -> Arc<RwLock<Instance>> { - let world_name = self.component::<WorldName>(); + let world_name = self.component::<InstanceName>(); let ecs = self.ecs.lock(); let instance_container = ecs.resource::<InstanceContainer>(); instance_container.get(&world_name).unwrap() @@ -495,7 +500,7 @@ impl Client { /// Returns whether we have a received the login packet yet. pub fn logged_in(&self) -> bool { // the login packet tells us the world name - self.query::<Option<&WorldName>>(&mut self.ecs.lock()) + self.query::<Option<&InstanceName>>(&mut self.ecs.lock()) .is_some() } @@ -560,6 +565,10 @@ pub struct JoinedClientBundle { pub current_sequence_number: CurrentSequenceNumber, pub last_sent_direction: LastSentLookDirection, pub abilities: PlayerAbilities, + pub permission_level: PermissionLevel, + + pub mining: mining::MineBundle, + pub _local: Local, } @@ -660,7 +669,7 @@ pub async fn tick_run_schedule_loop(run_schedule_sender: mpsc::UnboundedSender<( #[derive(Resource, Deref)] pub struct TickBroadcast(broadcast::Sender<()>); -fn send_tick_broadcast(tick_broadcast: ResMut<TickBroadcast>) { +pub fn send_tick_broadcast(tick_broadcast: ResMut<TickBroadcast>) { let _ = tick_broadcast.0.send(()); } /// A plugin that makes the [`RanScheduleBroadcast`] resource available. @@ -706,6 +715,7 @@ impl PluginGroup for DefaultPlugins { .add(PlayerMovePlugin) .add(InteractPlugin) .add(RespawnPlugin) + .add(MinePlugin) .add(TickBroadcastPlugin); #[cfg(feature = "log")] { diff --git a/azalea-client/src/entity_query.rs b/azalea-client/src/entity_query.rs index 8fe94659..0320457f 100644 --- a/azalea-client/src/entity_query.rs +++ b/azalea-client/src/entity_query.rs @@ -15,10 +15,10 @@ impl Client { /// /// # Examples /// ``` - /// # use azalea_world::entity::WorldName; + /// # use azalea_world::InstanceName; /// # fn example(mut client: azalea_client::Client) { /// let is_logged_in = client - /// .query::<Option<&WorldName>>(&mut client.ecs.lock()) + /// .query::<Option<&InstanceName>>(&mut client.ecs.lock()) /// .is_some(); /// # } /// ``` @@ -39,7 +39,7 @@ impl Client { /// ``` /// use azalea_client::{Client, GameProfileComponent}; /// use bevy_ecs::query::With; - /// use azalea_world::entity::{Position, metadata::Player}; + /// use azalea_entity::{Position, metadata::Player}; /// /// # fn example(mut bot: Client, sender_name: String) { /// let entity = bot.entity_by::<With<Player>, (&GameProfileComponent,)>( diff --git a/azalea-client/src/events.rs b/azalea-client/src/events.rs index e97f3477..900f559f 100644 --- a/azalea-client/src/events.rs +++ b/azalea-client/src/events.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use azalea_protocol::packets::game::{ clientbound_player_combat_kill_packet::ClientboundPlayerCombatKillPacket, ClientboundGamePacket, }; -use azalea_world::entity::MinecraftEntityId; +use azalea_world::MinecraftEntityId; use bevy_app::{App, FixedUpdate, Plugin, Update}; use bevy_ecs::{component::Component, event::EventReader, query::Added, system::Query}; use derive_more::{Deref, DerefMut}; diff --git a/azalea-client/src/interact.rs b/azalea-client/src/interact.rs index 45b845f9..dc0213b0 100644 --- a/azalea-client/src/interact.rs +++ b/azalea-client/src/interact.rs @@ -1,22 +1,22 @@ +use std::ops::AddAssign; + use azalea_block::BlockState; use azalea_core::{BlockHitResult, BlockPos, Direction, GameMode, Vec3}; +use azalea_entity::{clamp_look_direction, view_vector, EyeHeight, LookDirection, Position}; use azalea_inventory::{ItemSlot, ItemSlotData}; use azalea_nbt::NbtList; use azalea_physics::clip::{BlockShapeType, ClipContext, FluidPickType}; use azalea_protocol::packets::game::{ serverbound_interact_packet::InteractionHand, + serverbound_swing_packet::ServerboundSwingPacket, serverbound_use_item_on_packet::{BlockHit, ServerboundUseItemOnPacket}, }; -use azalea_world::{ - entity::{clamp_look_direction, view_vector, EyeHeight, LookDirection, Position, WorldName}, - Instance, InstanceContainer, -}; +use azalea_world::{Instance, InstanceContainer, InstanceName}; use bevy_app::{App, Plugin, Update}; use bevy_ecs::{ component::Component, entity::Entity, - event::EventReader, - prelude::Event, + event::{Event, EventReader, EventWriter}, schedule::IntoSystemConfigs, system::{Commands, Query, Res}, }; @@ -24,8 +24,9 @@ use derive_more::{Deref, DerefMut}; use log::warn; use crate::{ + client::{PermissionLevel, PlayerAbilities}, inventory::InventoryComponent, - local_player::{handle_send_packet_event, LocalGameMode}, + local_player::{handle_send_packet_event, LocalGameMode, SendPacketEvent}, Client, LocalPlayer, }; @@ -33,15 +34,18 @@ use crate::{ pub struct InteractPlugin; impl Plugin for InteractPlugin { fn build(&self, app: &mut App) { - app.add_event::<BlockInteractEvent>().add_systems( - Update, - ( - update_hit_result_component.after(clamp_look_direction), - handle_block_interact_event, - ) - .before(handle_send_packet_event) - .chain(), - ); + app.add_event::<BlockInteractEvent>() + .add_event::<SwingArmEvent>() + .add_systems( + Update, + ( + update_hit_result_component.after(clamp_look_direction), + handle_block_interact_event, + handle_swing_arm_event, + ) + .before(handle_send_packet_event) + .chain(), + ); } } @@ -73,9 +77,15 @@ pub struct BlockInteractEvent { /// A component that contains the number of changes this client has made to /// blocks. -#[derive(Component, Copy, Clone, Debug, Default, Deref, DerefMut)] +#[derive(Component, Copy, Clone, Debug, Default, Deref)] pub struct CurrentSequenceNumber(u32); +impl AddAssign<u32> for CurrentSequenceNumber { + fn add_assign(&mut self, rhs: u32) { + self.0 += rhs; + } +} + /// A component that contains the block that the player is currently looking at. #[derive(Component, Clone, Debug, Deref, DerefMut)] pub struct HitResultComponent(BlockHitResult); @@ -89,14 +99,15 @@ pub fn handle_block_interact_event( )>, ) { for event in events.iter() { - let Ok((local_player, mut sequence_number, hit_result)) = query.get_mut(event.entity) else { + let Ok((local_player, mut sequence_number, hit_result)) = query.get_mut(event.entity) + else { warn!("Sent BlockInteractEvent for entity that isn't LocalPlayer"); continue; }; // TODO: check to make sure we're within the world border - **sequence_number += 1; + *sequence_number += 1; // minecraft also does the interaction client-side (so it looks like clicking a // button is instant) but we don't really need that @@ -143,7 +154,7 @@ fn update_hit_result_component( &Position, &EyeHeight, &LookDirection, - &WorldName, + &InstanceName, )>, instance_container: Res<InstanceContainer>, ) { @@ -246,10 +257,11 @@ pub fn check_block_can_be_broken_by_item_in_adventure_mode( .nbt .as_compound() .and_then(|nbt| nbt.get("tag").and_then(|nbt| nbt.as_compound())) - .and_then(|nbt| nbt.get("CanDestroy").and_then(|nbt| nbt.as_list())) else { - // no CanDestroy tag - return false; - }; + .and_then(|nbt| nbt.get("CanDestroy").and_then(|nbt| nbt.as_list())) + else { + // no CanDestroy tag + return false; + }; let NbtList::String(_can_destroy) = can_destroy else { // CanDestroy tag must be a list of strings @@ -265,3 +277,31 @@ pub fn check_block_can_be_broken_by_item_in_adventure_mode( // true } + +pub fn can_use_game_master_blocks( + abilities: &PlayerAbilities, + permission_level: &PermissionLevel, +) -> bool { + abilities.instant_break && **permission_level >= 2 +} + +/// Swing your arm. This is purely a visual effect and won't interact with +/// anything in the world. +#[derive(Event)] +pub struct SwingArmEvent { + pub entity: Entity, +} +fn handle_swing_arm_event( + mut events: EventReader<SwingArmEvent>, + mut send_packet_events: EventWriter<SendPacketEvent>, +) { + for event in events.iter() { + send_packet_events.send(SendPacketEvent { + entity: event.entity, + packet: ServerboundSwingPacket { + hand: InteractionHand::MainHand, + } + .get(), + }); + } +} diff --git a/azalea-client/src/inventory.rs b/azalea-client/src/inventory.rs index 25ce157e..6f7829de 100644 --- a/azalea-client/src/inventory.rs +++ b/azalea-client/src/inventory.rs @@ -101,6 +101,7 @@ pub struct InventoryComponent { /// the scroll wheel. pub selected_hotbar_slot: u8, } + impl InventoryComponent { /// Returns a reference to the currently active menu. If a container is open /// it'll return [`Self::container_menu`], otherwise @@ -220,10 +221,10 @@ impl InventoryComponent { loop { let Some(&next_slot) = quick_craft_slots_iter.next() else { - carried.count = carried_count; - self.carried = ItemSlot::Present(carried); - return self.reset_quick_craft(); - }; + carried.count = carried_count; + self.carried = ItemSlot::Present(carried); + return self.reset_quick_craft(); + }; slot = self.menu().slot(next_slot as usize).unwrap(); slot_index = next_slot; @@ -244,8 +245,8 @@ impl InventoryComponent { // get the ItemSlotData for the slot let ItemSlot::Present(slot) = slot else { - unreachable!("the loop above requires the slot to be present to break") - }; + unreachable!("the loop above requires the slot to be present to break") + }; // if self.can_drag_to(slot) { let mut new_carried = carried.clone(); @@ -480,8 +481,8 @@ impl InventoryComponent { // now extend the carried item let target_slot = &mut self.carried; let ItemSlot::Present(target_slot_item) = target_slot else { - unreachable!("target slot is not empty but is not present"); - }; + unreachable!("target slot is not empty but is not present"); + }; target_slot_item.count += taken_item.count(); } } @@ -512,13 +513,13 @@ fn can_item_quick_replace( ignore_item_count: bool, ) -> bool { let ItemSlot::Present(target_slot) = target_slot else { - return false; - }; + return false; + }; let ItemSlot::Present(item) = item else { // i *think* this is what vanilla does // not 100% sure lol probably doesn't matter though - return false; - }; + return false; + }; if !item.is_same_item_and_nbt(target_slot) { return false; diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs index c47c5e29..e36cb846 100644 --- a/azalea-client/src/lib.rs +++ b/azalea-client/src/lib.rs @@ -21,7 +21,7 @@ mod get_mc_dir; pub mod interact; pub mod inventory; mod local_player; -mod mining; +pub mod mining; mod movement; pub mod packet_handling; pub mod ping; diff --git a/azalea-client/src/local_player.rs b/azalea-client/src/local_player.rs index 4368c47c..594513db 100644 --- a/azalea-client/src/local_player.rs +++ b/azalea-client/src/local_player.rs @@ -2,11 +2,9 @@ use std::{io, sync::Arc}; use azalea_auth::game_profile::GameProfile; use azalea_core::{ChunkPos, GameMode}; +use azalea_entity::{Dead, Position}; use azalea_protocol::packets::game::ServerboundGamePacket; -use azalea_world::{ - entity::{self, Dead, WorldName}, - Instance, InstanceContainer, PartialInstance, -}; +use azalea_world::{Instance, InstanceContainer, InstanceName, PartialInstance}; use bevy_ecs::{ component::Component, entity::Entity, @@ -32,7 +30,7 @@ use crate::{ /// You can also use the [`Local`] marker component for queries if you're only /// checking for a local player and don't need the contents of this component. /// -/// [`Local`]: azalea_world::entity::Local +/// [`Local`]: azalea_entity::Local /// [`Client`]: crate::Client #[derive(Component)] pub struct LocalPlayer { @@ -135,7 +133,7 @@ impl Drop for LocalPlayer { /// Update the [`LocalPlayerInLoadedChunk`] component for all [`LocalPlayer`]s. pub fn update_in_loaded_chunk( mut commands: bevy_ecs::system::Commands, - query: Query<(Entity, &WorldName, &entity::Position)>, + query: Query<(Entity, &InstanceName, &Position)>, instance_container: Res<InstanceContainer>, ) { for (entity, local_player, position) in &query { 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 }); + } } diff --git a/azalea-client/src/movement.rs b/azalea-client/src/movement.rs index 2db392b4..0bda9b15 100644 --- a/azalea-client/src/movement.rs +++ b/azalea-client/src/movement.rs @@ -2,6 +2,8 @@ use crate::client::Client; use crate::local_player::{ update_in_loaded_chunk, LocalPlayer, LocalPlayerInLoadedChunk, PhysicsState, }; +use azalea_entity::{metadata::Sprinting, Attributes, Jumping}; +use azalea_entity::{LastSentPosition, LookDirection, Physics, Position}; use azalea_physics::{force_jump_listener, PhysicsSet}; use azalea_protocol::packets::game::serverbound_player_command_packet::ServerboundPlayerCommandPacket; use azalea_protocol::packets::game::{ @@ -10,10 +12,7 @@ use azalea_protocol::packets::game::{ serverbound_move_player_rot_packet::ServerboundMovePlayerRotPacket, serverbound_move_player_status_only_packet::ServerboundMovePlayerStatusOnlyPacket, }; -use azalea_world::{ - entity::{self, metadata::Sprinting, Attributes, Jumping, MinecraftEntityId}, - MoveEntityError, -}; +use azalea_world::{MinecraftEntityId, MoveEntityError}; use bevy_app::{App, FixedUpdate, Plugin, Update}; use bevy_ecs::prelude::Event; use bevy_ecs::{ @@ -89,7 +88,7 @@ impl Client { /// `y_rot` goes from -180 to 180, and `x_rot` goes from -90 to 90. pub fn set_direction(&mut self, y_rot: f32, x_rot: f32) { let mut ecs = self.ecs.lock(); - let mut look_direction = self.query::<&mut entity::LookDirection>(&mut ecs); + let mut look_direction = self.query::<&mut LookDirection>(&mut ecs); (look_direction.y_rot, look_direction.x_rot) = (y_rot, x_rot); } @@ -110,12 +109,12 @@ pub(crate) fn send_position( &MinecraftEntityId, &mut LocalPlayer, &mut PhysicsState, - &entity::Position, - &mut entity::LastSentPosition, - &mut entity::Physics, - &entity::LookDirection, + &Position, + &mut LastSentPosition, + &mut Physics, + &LookDirection, &mut LastSentLookDirection, - &entity::metadata::Sprinting, + &Sprinting, ), &LocalPlayerInLoadedChunk, >, @@ -223,7 +222,7 @@ impl LocalPlayer { fn send_sprinting_if_needed( &mut self, id: &MinecraftEntityId, - sprinting: &entity::metadata::Sprinting, + sprinting: &Sprinting, physics_state: &mut PhysicsState, ) { let was_sprinting = physics_state.was_sprinting; @@ -287,9 +286,9 @@ pub fn local_player_ai_step( mut query: Query< ( &mut PhysicsState, - &mut entity::Physics, - &mut entity::metadata::Sprinting, - &mut entity::Attributes, + &mut Physics, + &mut Sprinting, + &mut Attributes, ), With<LocalPlayerInLoadedChunk>, >, @@ -431,12 +430,12 @@ fn set_sprinting( if sprinting { attributes .speed - .insert(entity::attributes::sprinting_modifier()) + .insert(azalea_entity::attributes::sprinting_modifier()) .is_ok() } else { attributes .speed - .remove(&entity::attributes::sprinting_modifier().uuid) + .remove(&azalea_entity::attributes::sprinting_modifier().uuid) .is_none() } } diff --git a/azalea-client/src/packet_handling.rs b/azalea-client/src/packet_handling.rs index 2371d834..fd8d77e5 100644 --- a/azalea-client/src/packet_handling.rs +++ b/azalea-client/src/packet_handling.rs @@ -1,6 +1,11 @@ use std::{collections::HashSet, io::Cursor, sync::Arc}; use azalea_core::{ChunkPos, GameMode, ResourceLocation, Vec3}; +use azalea_entity::{ + metadata::{apply_metadata, Health, PlayerMetadataBundle}, + Dead, EntityBundle, EntityKind, EntityUpdateSet, LastSentPosition, LoadedBy, LookDirection, + Physics, PlayerBundle, Position, RelativeEntityUpdate, +}; use azalea_protocol::{ connect::{ReadConnection, WriteConnection}, packets::game::{ @@ -15,15 +20,7 @@ use azalea_protocol::{ }, read::ReadPacketError, }; -use azalea_world::{ - entity::{ - metadata::{apply_metadata, Health, PlayerMetadataBundle}, - Dead, EntityBundle, EntityKind, EntityUpdateSet, LastSentPosition, LookDirection, - MinecraftEntityId, Physics, PlayerBundle, Position, WorldName, - }, - entity::{LoadedBy, RelativeEntityUpdate}, - InstanceContainer, PartialInstance, -}; +use azalea_world::{InstanceContainer, InstanceName, MinecraftEntityId, PartialInstance}; use bevy_app::{App, First, Plugin, PreUpdate}; use bevy_ecs::{ component::Component, @@ -195,7 +192,7 @@ fn process_packet_events(ecs: &mut World) { Commands, Query<( &mut LocalPlayer, - Option<&mut WorldName>, + Option<&mut InstanceName>, &GameProfileComponent, &ClientInformation, )>, @@ -225,7 +222,7 @@ fn process_packet_events(ecs: &mut World) { } else { commands .entity(player_entity) - .insert(WorldName(new_world_name.clone())); + .insert(InstanceName(new_world_name.clone())); } // add this world to the instance_container (or don't if it's already // there) @@ -348,10 +345,16 @@ fn process_packet_events(ecs: &mut World) { )>, > = SystemState::new(ecs); let mut query = system_state.get_mut(ecs); - let Ok((local_player, mut physics, mut direction, mut position, mut last_sent_position)) = - query.get_mut(player_entity) else { - continue; - }; + let Ok(( + local_player, + mut physics, + mut direction, + mut position, + mut last_sent_position, + )) = query.get_mut(player_entity) + else { + continue; + }; let delta_movement = physics.delta; @@ -555,12 +558,12 @@ fn process_packet_events(ecs: &mut World) { ClientboundGamePacket::AddEntity(p) => { debug!("Got add entity packet {:?}", p); - let mut system_state: SystemState<(Commands, Query<Option<&WorldName>>)> = + let mut system_state: SystemState<(Commands, Query<Option<&InstanceName>>)> = SystemState::new(ecs); let (mut commands, mut query) = system_state.get_mut(ecs); let world_name = query.get_mut(player_entity).unwrap(); - if let Some(WorldName(world_name)) = world_name { + if let Some(InstanceName(world_name)) = world_name { let bundle = p.as_entity_bundle(world_name.clone()); let mut entity_commands = commands.spawn(( MinecraftEntityId(p.id), @@ -622,12 +625,12 @@ fn process_packet_events(ecs: &mut World) { #[allow(clippy::type_complexity)] let mut system_state: SystemState<( Commands, - Query<(&TabList, Option<&WorldName>)>, + Query<(&TabList, Option<&InstanceName>)>, )> = SystemState::new(ecs); let (mut commands, mut query) = system_state.get_mut(ecs); let (tab_list, world_name) = query.get_mut(player_entity).unwrap(); - if let Some(WorldName(world_name)) = world_name { + if let Some(InstanceName(world_name)) = world_name { let bundle = p.as_player_bundle(world_name.clone()); let mut spawned = commands.spawn(( MinecraftEntityId(p.id), diff --git a/azalea-client/src/player.rs b/azalea-client/src/player.rs index 999f2490..25ba0d8c 100755 --- a/azalea-client/src/player.rs +++ b/azalea-client/src/player.rs @@ -1,7 +1,7 @@ use azalea_auth::game_profile::GameProfile; use azalea_chat::FormattedText; use azalea_core::GameMode; -use azalea_world::entity::EntityInfos; +use azalea_entity::EntityInfos; use bevy_ecs::{ event::EventReader, system::{Commands, Res}, |
