From e21e1b97bf9337e9f4747cd1b545b1b3a03e2ce7 Mon Sep 17 00:00:00 2001 From: mat <27899617+mat-1@users.noreply.github.com> Date: Sat, 22 Feb 2025 21:45:26 -0600 Subject: Refactor azalea-client (#205) * start organizing packet_handling more by moving packet handlers into their own functions * finish writing all the handler functions for packets * use macro for generating match statement for packet handler functions * fix set_entity_data * update config state to also use handler functions * organize az-client file structure by moving things into plugins directory * fix merge issues --- Cargo.lock | 7 + Cargo.toml | 3 +- azalea-brigadier/src/command_dispatcher.rs | 21 +- azalea-client/Cargo.toml | 1 + azalea-client/src/attack.rs | 148 -- azalea-client/src/chat.rs | 299 ---- azalea-client/src/chunks.rs | 225 --- azalea-client/src/client.rs | 37 +- azalea-client/src/configuration.rs | 62 - azalea-client/src/disconnect.rs | 103 -- azalea-client/src/events.rs | 243 --- azalea-client/src/interact.rs | 369 ----- azalea-client/src/inventory.rs | 768 ---------- azalea-client/src/lib.rs | 16 +- azalea-client/src/local_player.rs | 13 +- azalea-client/src/mining.rs | 643 -------- azalea-client/src/movement.rs | 580 ------- azalea-client/src/packet_handling/configuration.rs | 271 ---- azalea-client/src/packet_handling/game.rs | 1585 -------------------- azalea-client/src/packet_handling/login.rs | 114 -- azalea-client/src/packet_handling/mod.rs | 78 - azalea-client/src/player.rs | 2 +- azalea-client/src/plugins/attack.rs | 149 ++ azalea-client/src/plugins/brand.rs | 63 + azalea-client/src/plugins/chat/handler.rs | 61 + azalea-client/src/plugins/chat/mod.rs | 240 +++ azalea-client/src/plugins/chunks.rs | 223 +++ azalea-client/src/plugins/disconnect.rs | 103 ++ azalea-client/src/plugins/events.rs | 259 ++++ azalea-client/src/plugins/interact.rs | 370 +++++ azalea-client/src/plugins/inventory.rs | 766 ++++++++++ azalea-client/src/plugins/mining.rs | 644 ++++++++ azalea-client/src/plugins/mod.rs | 14 + azalea-client/src/plugins/movement.rs | 580 +++++++ azalea-client/src/plugins/packet/config/events.rs | 90 ++ azalea-client/src/plugins/packet/config/mod.rs | 223 +++ azalea-client/src/plugins/packet/game/events.rs | 178 +++ azalea-client/src/plugins/packet/game/mod.rs | 1583 +++++++++++++++++++ azalea-client/src/plugins/packet/login.rs | 114 ++ azalea-client/src/plugins/packet/mod.rs | 109 ++ azalea-client/src/plugins/respawn.rs | 35 + azalea-client/src/plugins/task_pool.rs | 182 +++ azalea-client/src/plugins/tick_end.rs | 36 + azalea-client/src/respawn.rs | 34 - azalea-client/src/send_client_end.rs | 36 - azalea-client/src/task_pool.rs | 182 --- azalea-entity/src/lib.rs | 2 +- azalea-protocol/src/packets/game/c_add_entity.rs | 6 +- .../src/packets/game/c_set_entity_motion.rs | 5 +- azalea/examples/testbot/commands/debug.rs | 6 +- azalea/src/accept_resource_packs.rs | 4 +- azalea/src/auto_respawn.rs | 2 +- azalea/src/container.rs | 4 +- azalea/src/pathfinder/simulation.rs | 6 +- 54 files changed, 6076 insertions(+), 5821 deletions(-) delete mode 100644 azalea-client/src/attack.rs delete mode 100755 azalea-client/src/chat.rs delete mode 100644 azalea-client/src/chunks.rs delete mode 100644 azalea-client/src/configuration.rs delete mode 100644 azalea-client/src/disconnect.rs delete mode 100644 azalea-client/src/events.rs delete mode 100644 azalea-client/src/interact.rs delete mode 100644 azalea-client/src/inventory.rs delete mode 100644 azalea-client/src/mining.rs delete mode 100644 azalea-client/src/movement.rs delete mode 100644 azalea-client/src/packet_handling/configuration.rs delete mode 100644 azalea-client/src/packet_handling/game.rs delete mode 100644 azalea-client/src/packet_handling/login.rs delete mode 100644 azalea-client/src/packet_handling/mod.rs create mode 100644 azalea-client/src/plugins/attack.rs create mode 100644 azalea-client/src/plugins/brand.rs create mode 100644 azalea-client/src/plugins/chat/handler.rs create mode 100644 azalea-client/src/plugins/chat/mod.rs create mode 100644 azalea-client/src/plugins/chunks.rs create mode 100644 azalea-client/src/plugins/disconnect.rs create mode 100644 azalea-client/src/plugins/events.rs create mode 100644 azalea-client/src/plugins/interact.rs create mode 100644 azalea-client/src/plugins/inventory.rs create mode 100644 azalea-client/src/plugins/mining.rs create mode 100644 azalea-client/src/plugins/mod.rs create mode 100644 azalea-client/src/plugins/movement.rs create mode 100644 azalea-client/src/plugins/packet/config/events.rs create mode 100644 azalea-client/src/plugins/packet/config/mod.rs create mode 100644 azalea-client/src/plugins/packet/game/events.rs create mode 100644 azalea-client/src/plugins/packet/game/mod.rs create mode 100644 azalea-client/src/plugins/packet/login.rs create mode 100644 azalea-client/src/plugins/packet/mod.rs create mode 100644 azalea-client/src/plugins/respawn.rs create mode 100644 azalea-client/src/plugins/task_pool.rs create mode 100644 azalea-client/src/plugins/tick_end.rs delete mode 100644 azalea-client/src/respawn.rs delete mode 100644 azalea-client/src/send_client_end.rs delete mode 100644 azalea-client/src/task_pool.rs diff --git a/Cargo.lock b/Cargo.lock index e37ae92d..58709793 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -356,6 +356,7 @@ dependencies = [ "derive_more 2.0.1", "minecraft_folder_path", "parking_lot", + "paste", "regex", "reqwest", "simdnbt", @@ -2219,6 +2220,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pem-rfc7468" version = "0.7.0" diff --git a/Cargo.toml b/Cargo.toml index f549d16f..d4cb4331 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,6 @@ repository = "https://github.com/azalea-rs/azalea" aes = "0.8.4" anyhow = "1.0.95" async-recursion = "1.1.1" -async-trait = "0.1.86" base64 = "0.22.1" bevy_app = "0.15.2" bevy_ecs = { version = "0.15.2", default-features = false } @@ -49,7 +48,6 @@ env_logger = "0.11.6" flate2 = "1.0.35" futures = "0.3.31" futures-lite = "2.6.0" -log = "0.4.25" md-5 = "0.10.6" minecraft_folder_path = "0.1.2" nohash-hasher = "0.2.0" @@ -80,6 +78,7 @@ hickory-resolver = { version = "0.24.3", default-features = false } uuid = "1.12.1" num-format = "0.4.4" indexmap = "2.7.1" +paste = "1.0.15" compact_str = "0.8.1" # --- Profile Settings --- diff --git a/azalea-brigadier/src/command_dispatcher.rs b/azalea-brigadier/src/command_dispatcher.rs index 15648b42..eaf4a5e0 100755 --- a/azalea-brigadier/src/command_dispatcher.rs +++ b/azalea-brigadier/src/command_dispatcher.rs @@ -288,21 +288,16 @@ impl CommandDispatcher { next.push(child.copy_for(context.source.clone())); } } - } else { - match &context.command { - Some(context_command) => { - found_command = true; + } else if let Some(context_command) = &context.command { + found_command = true; - let value = context_command(context); - result += value; - // consumer.on_command_complete(context, true, value); - successful_forks += 1; + let value = context_command(context); + result += value; + // consumer.on_command_complete(context, true, value); + successful_forks += 1; - // TODO: allow context_command to error and handle - // those errors - } - _ => {} - } + // TODO: allow context_command to error and handle + // those errors } } diff --git a/azalea-client/Cargo.toml b/azalea-client/Cargo.toml index ea146970..3476f82e 100644 --- a/azalea-client/Cargo.toml +++ b/azalea-client/Cargo.toml @@ -27,6 +27,7 @@ bevy_time.workspace = true derive_more = { workspace = true, features = ["deref", "deref_mut"] } minecraft_folder_path.workspace = true parking_lot.workspace = true +paste.workspace = true regex.workspace = true reqwest.workspace = true simdnbt.workspace = true diff --git a/azalea-client/src/attack.rs b/azalea-client/src/attack.rs deleted file mode 100644 index 0f5a8305..00000000 --- a/azalea-client/src/attack.rs +++ /dev/null @@ -1,148 +0,0 @@ -use azalea_core::{game_type::GameMode, tick::GameTick}; -use azalea_entity::{ - Attributes, Physics, - metadata::{ShiftKeyDown, Sprinting}, - update_bounding_box, -}; -use azalea_physics::PhysicsSet; -use azalea_protocol::packets::game::s_interact::{self, ServerboundInteract}; -use azalea_world::MinecraftEntityId; -use bevy_app::{App, Plugin, Update}; -use bevy_ecs::prelude::*; -use derive_more::{Deref, DerefMut}; - -use crate::{ - Client, interact::SwingArmEvent, local_player::LocalGameMode, movement::MoveEventsSet, - packet_handling::game::SendPacketEvent, respawn::perform_respawn, -}; - -pub struct AttackPlugin; -impl Plugin for AttackPlugin { - fn build(&self, app: &mut App) { - app.add_event::() - .add_systems( - Update, - handle_attack_event - .before(update_bounding_box) - .before(MoveEventsSet) - .after(perform_respawn), - ) - .add_systems( - GameTick, - ( - increment_ticks_since_last_attack, - update_attack_strength_scale.after(PhysicsSet), - ) - .chain(), - ); - } -} - -impl Client { - /// Attack the entity with the given id. - pub fn attack(&mut self, entity_id: MinecraftEntityId) { - self.ecs.lock().send_event(AttackEvent { - entity: self.entity, - target: entity_id, - }); - } - - /// Whether the player has an attack cooldown. - pub fn has_attack_cooldown(&self) -> bool { - let Some(AttackStrengthScale(ticks_since_last_attack)) = - self.get_component::() - else { - // they don't even have an AttackStrengthScale so they probably can't attack - // lmao, just return false - return false; - }; - ticks_since_last_attack < 1.0 - } -} - -#[derive(Event)] -pub struct AttackEvent { - pub entity: Entity, - pub target: MinecraftEntityId, -} -pub fn handle_attack_event( - mut events: EventReader, - mut query: Query<( - &LocalGameMode, - &mut TicksSinceLastAttack, - &mut Physics, - &mut Sprinting, - &mut ShiftKeyDown, - )>, - mut send_packet_events: EventWriter, - mut swing_arm_event: EventWriter, -) { - for event in events.read() { - let (game_mode, mut ticks_since_last_attack, mut physics, mut sprinting, sneaking) = - query.get_mut(event.entity).unwrap(); - - swing_arm_event.send(SwingArmEvent { - entity: event.entity, - }); - send_packet_events.send(SendPacketEvent::new( - event.entity, - ServerboundInteract { - entity_id: event.target, - action: s_interact::ActionType::Attack, - using_secondary_action: **sneaking, - }, - )); - - // we can't attack if we're in spectator mode but it still sends the attack - // packet - if game_mode.current == GameMode::Spectator { - continue; - }; - - ticks_since_last_attack.0 = 0; - - physics.velocity = physics.velocity.multiply(0.6, 1.0, 0.6); - **sprinting = false; - } -} - -#[derive(Default, Bundle)] -pub struct AttackBundle { - pub ticks_since_last_attack: TicksSinceLastAttack, - pub attack_strength_scale: AttackStrengthScale, -} - -#[derive(Default, Component, Clone, Deref, DerefMut)] -pub struct TicksSinceLastAttack(pub u32); -pub fn increment_ticks_since_last_attack(mut query: Query<&mut TicksSinceLastAttack>) { - for mut ticks_since_last_attack in query.iter_mut() { - **ticks_since_last_attack += 1; - } -} - -#[derive(Default, Component, Clone, Deref, DerefMut)] -pub struct AttackStrengthScale(pub f32); -pub fn update_attack_strength_scale( - mut query: Query<(&TicksSinceLastAttack, &Attributes, &mut AttackStrengthScale)>, -) { - for (ticks_since_last_attack, attributes, mut attack_strength_scale) in query.iter_mut() { - // look 0.5 ticks into the future because that's what vanilla does - **attack_strength_scale = - get_attack_strength_scale(ticks_since_last_attack.0, attributes, 0.5); - } -} - -/// Returns how long it takes for the attack cooldown to reset (in ticks). -pub fn get_attack_strength_delay(attributes: &Attributes) -> f32 { - ((1. / attributes.attack_speed.calculate()) * 20.) as f32 -} - -pub fn get_attack_strength_scale( - ticks_since_last_attack: u32, - attributes: &Attributes, - in_ticks: f32, -) -> f32 { - let attack_strength_delay = get_attack_strength_delay(attributes); - let attack_strength = (ticks_since_last_attack as f32 + in_ticks) / attack_strength_delay; - attack_strength.clamp(0., 1.) -} diff --git a/azalea-client/src/chat.rs b/azalea-client/src/chat.rs deleted file mode 100755 index 2bef9570..00000000 --- a/azalea-client/src/chat.rs +++ /dev/null @@ -1,299 +0,0 @@ -//! Implementations of chat-related features. - -use std::{ - sync::Arc, - time::{SystemTime, UNIX_EPOCH}, -}; - -use azalea_chat::FormattedText; -use azalea_protocol::packets::{ - Packet, - game::{ - c_disguised_chat::ClientboundDisguisedChat, - c_player_chat::ClientboundPlayerChat, - c_system_chat::ClientboundSystemChat, - s_chat::{LastSeenMessagesUpdate, ServerboundChat}, - s_chat_command::ServerboundChatCommand, - }, -}; -use bevy_app::{App, Plugin, Update}; -use bevy_ecs::{ - entity::Entity, - event::{EventReader, EventWriter}, - prelude::Event, - schedule::IntoSystemConfigs, -}; -use uuid::Uuid; - -use crate::{ - client::Client, - packet_handling::game::{SendPacketEvent, handle_send_packet_event}, -}; - -/// A chat packet, either a system message or a chat message. -#[derive(Debug, Clone, PartialEq)] -pub enum ChatPacket { - System(Arc), - Player(Arc), - Disguised(Arc), -} - -macro_rules! regex { - ($re:literal $(,)?) => {{ - static RE: std::sync::LazyLock = - std::sync::LazyLock::new(|| regex::Regex::new($re).unwrap()); - &RE - }}; -} - -impl ChatPacket { - /// Get the message shown in chat for this packet. - pub fn message(&self) -> FormattedText { - match self { - ChatPacket::System(p) => p.content.clone(), - ChatPacket::Player(p) => p.message(), - ChatPacket::Disguised(p) => p.message(), - } - } - - /// Determine the username of the sender and content of the message. This - /// does not preserve formatting codes. If it's not a player-sent chat - /// message or the sender couldn't be determined, the username part will be - /// None. - pub fn split_sender_and_content(&self) -> (Option, String) { - match self { - ChatPacket::System(p) => { - let message = p.content.to_string(); - // Overlay messages aren't in chat - if p.overlay { - return (None, message); - } - // It's a system message, so we'll have to match the content - // with regex - if let Some(m) = regex!("^<([a-zA-Z_0-9]{1,16})> (.+)$").captures(&message) { - return (Some(m[1].to_string()), m[2].to_string()); - } - - (None, message) - } - ChatPacket::Player(p) => ( - // If it's a player chat packet, then the sender and content - // are already split for us. - Some(p.chat_type.name.to_string()), - p.body.content.clone(), - ), - ChatPacket::Disguised(p) => ( - // disguised chat packets are basically the same as player chat packets but without - // the chat signing things - Some(p.chat_type.name.to_string()), - p.message.to_string(), - ), - } - } - - /// Get the username of the sender of the message. If it's not a - /// player-sent chat message or the sender couldn't be determined, this - /// will be None. - pub fn username(&self) -> Option { - self.split_sender_and_content().0 - } - - /// Get the UUID of the sender of the message. If it's not a - /// player-sent chat message, this will be None (this is sometimes the case - /// when a server uses a plugin to modify chat messages). - pub fn uuid(&self) -> Option { - match self { - ChatPacket::System(_) => None, - ChatPacket::Player(m) => Some(m.sender), - ChatPacket::Disguised(_) => None, - } - } - - /// Get the content part of the message as a string. This does not preserve - /// formatting codes. If it's not a player-sent chat message or the sender - /// couldn't be determined, this will contain the entire message. - pub fn content(&self) -> String { - self.split_sender_and_content().1 - } - - /// Create a new Chat from a string. This is meant to be used as a - /// convenience function for testing. - pub fn new(message: &str) -> Self { - ChatPacket::System(Arc::new(ClientboundSystemChat { - content: FormattedText::from(message), - overlay: false, - })) - } - - /// Whether this message was sent with /msg (or aliases). It works by - /// checking the translation key, so it won't work on servers that use their - /// own whisper system. - pub fn is_whisper(&self) -> bool { - match self.message() { - FormattedText::Text(_) => false, - FormattedText::Translatable(t) => t.key == "commands.message.display.incoming", - } - } -} - -impl Client { - /// Send a chat message to the server. This only sends the chat packet and - /// not the command packet, which means on some servers you can use this to - /// send chat messages that start with a `/`. The [`Client::chat`] function - /// handles checking whether the message is a command and using the - /// proper packet for you, so you should use that instead. - pub fn send_chat_packet(&self, message: &str) { - self.ecs.lock().send_event(SendChatKindEvent { - entity: self.entity, - content: message.to_string(), - kind: ChatKind::Message, - }); - self.run_schedule_sender.send(()).unwrap(); - } - - /// Send a command packet to the server. The `command` argument should not - /// include the slash at the front. - /// - /// You can also just use [`Client::chat`] and start your message with a `/` - /// to send a command. - pub fn send_command_packet(&self, command: &str) { - self.ecs.lock().send_event(SendChatKindEvent { - entity: self.entity, - content: command.to_string(), - kind: ChatKind::Command, - }); - self.run_schedule_sender.send(()).unwrap(); - } - - /// Send a message in chat. - /// - /// ```rust,no_run - /// # use azalea_client::Client; - /// # async fn example(bot: Client) -> anyhow::Result<()> { - /// bot.chat("Hello, world!"); - /// # Ok(()) - /// # } - /// ``` - pub fn chat(&self, content: &str) { - self.ecs.lock().send_event(SendChatEvent { - entity: self.entity, - content: content.to_string(), - }); - self.run_schedule_sender.send(()).unwrap(); - } -} - -pub struct ChatPlugin; -impl Plugin for ChatPlugin { - fn build(&self, app: &mut App) { - app.add_event::() - .add_event::() - .add_event::() - .add_systems( - Update, - ( - handle_send_chat_event, - handle_send_chat_kind_event.after(handle_send_packet_event), - ) - .chain(), - ); - } -} - -/// A client received a chat message packet. -#[derive(Event, Debug, Clone)] -pub struct ChatReceivedEvent { - pub entity: Entity, - pub packet: ChatPacket, -} - -/// Send a chat message (or command, if it starts with a slash) to the server. -#[derive(Event)] -pub struct SendChatEvent { - pub entity: Entity, - pub content: String, -} - -pub fn handle_send_chat_event( - mut events: EventReader, - mut send_chat_kind_events: EventWriter, -) { - for event in events.read() { - if event.content.starts_with('/') { - send_chat_kind_events.send(SendChatKindEvent { - entity: event.entity, - content: event.content[1..].to_string(), - kind: ChatKind::Command, - }); - } else { - send_chat_kind_events.send(SendChatKindEvent { - entity: event.entity, - content: event.content.clone(), - kind: ChatKind::Message, - }); - } - } -} - -/// Send a chat packet to the server of a specific kind (chat message or -/// command). Usually you just want [`SendChatEvent`] instead. -/// -/// Usually setting the kind to `Message` will make it send a chat message even -/// if it starts with a slash, but some server implementations will always do a -/// command if it starts with a slash. -/// -/// If you're wondering why this isn't two separate events, it's so ordering is -/// preserved if multiple chat messages and commands are sent at the same time. -#[derive(Event)] -pub struct SendChatKindEvent { - pub entity: Entity, - pub content: String, - pub kind: ChatKind, -} - -/// A kind of chat packet, either a chat message or a command. -pub enum ChatKind { - Message, - Command, -} - -pub fn handle_send_chat_kind_event( - mut events: EventReader, - mut send_packet_events: EventWriter, -) { - for event in events.read() { - let content = event - .content - .chars() - .filter(|c| !matches!(c, '\x00'..='\x1F' | '\x7F' | 'ยง')) - .take(256) - .collect::(); - let packet = match event.kind { - ChatKind::Message => ServerboundChat { - message: content, - timestamp: SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time shouldn't be before epoch") - .as_millis() - .try_into() - .expect("Instant should fit into a u64"), - salt: azalea_crypto::make_salt(), - signature: None, - last_seen_messages: LastSeenMessagesUpdate::default(), - } - .into_variant(), - ChatKind::Command => { - // TODO: chat signing - ServerboundChatCommand { command: content }.into_variant() - } - }; - - send_packet_events.send(SendPacketEvent::new(event.entity, packet)); - } -} - -// TODO -// MessageSigner, ChatMessageContent, LastSeenMessages -// fn sign_message() -> MessageSignature { -// MessageSignature::default() -// } diff --git a/azalea-client/src/chunks.rs b/azalea-client/src/chunks.rs deleted file mode 100644 index 67313757..00000000 --- a/azalea-client/src/chunks.rs +++ /dev/null @@ -1,225 +0,0 @@ -//! Used for Minecraft's chunk batching introduced in 23w31a (1.20.2). It's used -//! for making the server spread out how often it sends us chunk packets -//! depending on our receiving speed. - -use std::{ - io::Cursor, - ops::Deref, - time::{Duration, Instant}, -}; - -use azalea_core::position::ChunkPos; -use azalea_protocol::packets::game::{ - c_level_chunk_with_light::ClientboundLevelChunkWithLight, - s_chunk_batch_received::ServerboundChunkBatchReceived, -}; -use bevy_app::{App, Plugin, Update}; -use bevy_ecs::prelude::*; -use simdnbt::owned::BaseNbt; -use tracing::{error, trace}; - -use crate::{ - InstanceHolder, - interact::handle_block_interact_event, - inventory::InventorySet, - packet_handling::game::{SendPacketEvent, handle_send_packet_event}, - respawn::perform_respawn, -}; - -pub struct ChunkPlugin; -impl Plugin for ChunkPlugin { - fn build(&self, app: &mut App) { - app.add_systems( - Update, - ( - handle_chunk_batch_start_event, - handle_receive_chunk_events, - handle_chunk_batch_finished_event, - ) - .chain() - .before(handle_send_packet_event) - .before(InventorySet) - .before(handle_block_interact_event) - .before(perform_respawn), - ) - .add_event::() - .add_event::() - .add_event::(); - } -} - -#[derive(Event)] -pub struct ReceiveChunkEvent { - pub entity: Entity, - pub packet: ClientboundLevelChunkWithLight, -} - -#[derive(Component, Clone, Debug)] -pub struct ChunkBatchInfo { - pub start_time: Instant, - pub aggregated_duration_per_chunk: Duration, - pub old_samples_weight: u32, -} - -#[derive(Event)] -pub struct ChunkBatchStartEvent { - pub entity: Entity, -} -#[derive(Event)] -pub struct ChunkBatchFinishedEvent { - pub entity: Entity, - pub batch_size: u32, -} - -pub fn handle_receive_chunk_events( - mut events: EventReader, - mut query: Query<&mut InstanceHolder>, -) { - for event in events.read() { - let pos = ChunkPos::new(event.packet.x, event.packet.z); - - let local_player = query.get_mut(event.entity).unwrap(); - - let mut instance = local_player.instance.write(); - let mut partial_instance = local_player.partial_instance.write(); - - // OPTIMIZATION: if we already know about the chunk from the shared world (and - // not ourselves), then we don't need to parse it again. This is only used when - // we have a shared world, since we check that the chunk isn't currently owned - // by this client. - let shared_chunk = instance.chunks.get(&pos); - let this_client_has_chunk = partial_instance.chunks.limited_get(&pos).is_some(); - - if !this_client_has_chunk { - if let Some(shared_chunk) = shared_chunk { - trace!("Skipping parsing chunk {pos:?} because we already know about it"); - partial_instance - .chunks - .limited_set(&pos, Some(shared_chunk)); - continue; - } - } - - let heightmaps_nbt = &event.packet.chunk_data.heightmaps; - // necessary to make the unwrap_or work - let empty_nbt = BaseNbt::default(); - let heightmaps = heightmaps_nbt.unwrap_or(&empty_nbt).deref(); - - if let Err(e) = partial_instance.chunks.replace_with_packet_data( - &pos, - &mut Cursor::new(&event.packet.chunk_data.data), - heightmaps, - &mut instance.chunks, - ) { - error!( - "Couldn't set chunk data: {e}. World height: {}", - instance.chunks.height - ); - } - } -} - -impl ChunkBatchInfo { - pub fn batch_finished(&mut self, batch_size: u32) { - if batch_size == 0 { - return; - } - let batch_duration = self.start_time.elapsed(); - let duration_per_chunk = batch_duration / batch_size; - let clamped_duration = Duration::clamp( - duration_per_chunk, - self.aggregated_duration_per_chunk / 3, - self.aggregated_duration_per_chunk * 3, - ); - self.aggregated_duration_per_chunk = - ((self.aggregated_duration_per_chunk * self.old_samples_weight) + clamped_duration) - / (self.old_samples_weight + 1); - self.old_samples_weight = u32::min(49, self.old_samples_weight + 1); - } - - pub fn desired_chunks_per_tick(&self) -> f32 { - (7000000. / self.aggregated_duration_per_chunk.as_nanos() as f64) as f32 - } -} - -pub fn handle_chunk_batch_start_event( - mut query: Query<&mut ChunkBatchInfo>, - mut events: EventReader, -) { - for event in events.read() { - if let Ok(mut chunk_batch_info) = query.get_mut(event.entity) { - chunk_batch_info.start_time = Instant::now(); - } - } -} - -pub fn handle_chunk_batch_finished_event( - mut query: Query<&mut ChunkBatchInfo>, - mut events: EventReader, - mut send_packets: EventWriter, -) { - for event in events.read() { - if let Ok(mut chunk_batch_info) = query.get_mut(event.entity) { - chunk_batch_info.batch_finished(event.batch_size); - let desired_chunks_per_tick = chunk_batch_info.desired_chunks_per_tick(); - send_packets.send(SendPacketEvent::new( - event.entity, - ServerboundChunkBatchReceived { - desired_chunks_per_tick, - }, - )); - } - } -} - -#[derive(Clone, Debug)] -pub struct ChunkReceiveSpeedAccumulator { - batch_sizes: Vec, - /// as milliseconds - batch_durations: Vec, - index: usize, - filled_size: usize, -} -impl ChunkReceiveSpeedAccumulator { - pub fn new(capacity: usize) -> Self { - Self { - batch_sizes: vec![0; capacity], - batch_durations: vec![0; capacity], - index: 0, - filled_size: 0, - } - } - - pub fn accumulate(&mut self, batch_size: u32, batch_duration: Duration) { - self.batch_sizes[self.index] = batch_size; - self.batch_durations[self.index] = - f32::clamp(batch_duration.as_millis() as f32, 0., 15000.) as u32; - self.index = (self.index + 1) % self.batch_sizes.len(); - if self.filled_size < self.batch_sizes.len() { - self.filled_size += 1; - } - } - - pub fn get_millis_per_chunk(&self) -> f64 { - let mut total_batch_size = 0; - let mut total_batch_duration = 0; - for i in 0..self.filled_size { - total_batch_size += self.batch_sizes[i]; - total_batch_duration += self.batch_durations[i]; - } - if total_batch_size == 0 { - return 0.; - } - total_batch_duration as f64 / total_batch_size as f64 - } -} - -impl Default for ChunkBatchInfo { - fn default() -> Self { - Self { - start_time: Instant::now(), - aggregated_duration_per_chunk: Duration::from_millis(2), - old_samples_weight: 1, - } - } -} diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index 2f7460f5..7a1c3ae0 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -63,28 +63,27 @@ use uuid::Uuid; use crate::{ Account, PlayerInfo, attack::{self, AttackPlugin}, + brand::BrandPlugin, chat::ChatPlugin, - chunks::{ChunkBatchInfo, ChunkPlugin}, - configuration::ConfigurationPlugin, + chunks::{ChunkBatchInfo, ChunksPlugin}, disconnect::{DisconnectEvent, DisconnectPlugin}, - events::{Event, EventPlugin, LocalPlayerEvents}, + events::{Event, EventsPlugin, LocalPlayerEvents}, interact::{CurrentSequenceNumber, InteractPlugin}, inventory::{Inventory, InventoryPlugin}, local_player::{ GameProfileComponent, Hunger, InstanceHolder, PermissionLevel, PlayerAbilities, TabList, - death_event, }, - mining::{self, MinePlugin}, - movement::{LastSentLookDirection, PhysicsState, PlayerMovePlugin}, - packet_handling::{ - PacketHandlerPlugin, + mining::{self, MiningPlugin}, + movement::{LastSentLookDirection, MovementPlugin, PhysicsState}, + packet::{ + PacketPlugin, login::{self, InLoginState, LoginSendPacketQueue}, }, player::retroactively_add_game_profile_component, raw_connection::RawConnection, respawn::RespawnPlugin, - send_client_end::TickEndPlugin, task_pool::TaskPoolPlugin, + tick_end::TickEndPlugin, }; /// `Client` has the things that a user interacting with the library will want. @@ -370,7 +369,7 @@ impl Client { let (ecs_packets_tx, mut ecs_packets_rx) = mpsc::unbounded_channel(); ecs_lock.lock().entity_mut(entity).insert(( LoginSendPacketQueue { tx: ecs_packets_tx }, - login::IgnoreQueryIds::default(), + crate::packet::login::IgnoreQueryIds::default(), InLoginState, )); @@ -468,7 +467,7 @@ impl Client { ClientboundLoginPacket::CustomQuery(p) => { debug!("Got custom query {:?}", p); // replying to custom query is done in - // packet_handling::login::process_packet_events + // packet::login::process_packet_events } ClientboundLoginPacket::CookieRequest(p) => { debug!("Got cookie request {:?}", p); @@ -794,7 +793,7 @@ pub struct LocalPlayerBundle { /// A bundle for the components that are present on a local player that is /// currently in the `game` protocol state. If you want to filter for this, just /// use [`LocalEntity`]. -#[derive(Bundle)] +#[derive(Bundle, Default)] pub struct JoinedClientBundle { // note that InstanceHolder isn't here because it's set slightly before we fully join the world pub physics_state: PhysicsState, @@ -826,8 +825,6 @@ impl Plugin for AzaleaPlugin { app.add_systems( Update, ( - // fire the Death event when the player dies. - death_event, // add GameProfileComponent when we get an AddPlayerEvent retroactively_add_game_profile_component.after(EntityUpdateSet::Index), ), @@ -972,23 +969,23 @@ impl PluginGroup for DefaultPlugins { let mut group = PluginGroupBuilder::start::() .add(AmbiguityLoggerPlugin) .add(TimePlugin) - .add(PacketHandlerPlugin) + .add(PacketPlugin) .add(AzaleaPlugin) .add(EntityPlugin) .add(PhysicsPlugin) - .add(EventPlugin) + .add(EventsPlugin) .add(TaskPoolPlugin::default()) .add(InventoryPlugin) .add(ChatPlugin) .add(DisconnectPlugin) - .add(PlayerMovePlugin) + .add(MovementPlugin) .add(InteractPlugin) .add(RespawnPlugin) - .add(MinePlugin) + .add(MiningPlugin) .add(AttackPlugin) - .add(ChunkPlugin) + .add(ChunksPlugin) .add(TickEndPlugin) - .add(ConfigurationPlugin) + .add(BrandPlugin) .add(TickBroadcastPlugin); #[cfg(feature = "log")] { diff --git a/azalea-client/src/configuration.rs b/azalea-client/src/configuration.rs deleted file mode 100644 index d578be7a..00000000 --- a/azalea-client/src/configuration.rs +++ /dev/null @@ -1,62 +0,0 @@ -use azalea_buf::AzaleaWrite; -use azalea_core::resource_location::ResourceLocation; -use azalea_protocol::{ - common::client_information::ClientInformation, - packets::config::{ - s_client_information::ServerboundClientInformation, - s_custom_payload::ServerboundCustomPayload, - }, -}; -use bevy_app::prelude::*; -use bevy_ecs::prelude::*; -use tracing::{debug, warn}; - -use crate::packet_handling::{configuration::SendConfigurationEvent, login::InLoginState}; - -pub struct ConfigurationPlugin; -impl Plugin for ConfigurationPlugin { - fn build(&self, app: &mut App) { - app.add_systems( - Update, - handle_end_login_state - .before(crate::packet_handling::configuration::handle_send_packet_event), - ); - } -} - -fn handle_end_login_state( - mut removed: RemovedComponents, - query: Query<&ClientInformation>, - mut send_packet_events: EventWriter, -) { - for entity in removed.read() { - let mut brand_data = Vec::new(); - // they don't have to know :) - "vanilla".azalea_write(&mut brand_data).unwrap(); - send_packet_events.send(SendConfigurationEvent::new( - entity, - ServerboundCustomPayload { - identifier: ResourceLocation::new("brand"), - data: brand_data.into(), - }, - )); - - let client_information = match query.get(entity).ok() { - Some(i) => i, - None => { - warn!( - "ClientInformation component was not set before leaving login state, using a default" - ); - &ClientInformation::default() - } - }; - - debug!("Writing ClientInformation while in config state: {client_information:?}"); - send_packet_events.send(SendConfigurationEvent::new( - entity, - ServerboundClientInformation { - information: client_information.clone(), - }, - )); - } -} diff --git a/azalea-client/src/disconnect.rs b/azalea-client/src/disconnect.rs deleted file mode 100644 index bd10ac75..00000000 --- a/azalea-client/src/disconnect.rs +++ /dev/null @@ -1,103 +0,0 @@ -//! Disconnect a client from the server. - -use azalea_chat::FormattedText; -use azalea_entity::{EntityBundle, InLoadedChunk, LocalEntity, metadata::PlayerMetadataBundle}; -use bevy_app::{App, Plugin, PostUpdate}; -use bevy_ecs::{ - component::Component, - entity::Entity, - event::{EventReader, EventWriter}, - prelude::Event, - query::{Changed, With}, - schedule::IntoSystemConfigs, - system::{Commands, Query}, -}; -use derive_more::Deref; -use tracing::trace; - -use crate::{ - InstanceHolder, client::JoinedClientBundle, events::LocalPlayerEvents, - raw_connection::RawConnection, -}; - -pub struct DisconnectPlugin; -impl Plugin for DisconnectPlugin { - fn build(&self, app: &mut App) { - app.add_event::().add_systems( - PostUpdate, - ( - update_read_packets_task_running_component, - disconnect_on_connection_dead, - remove_components_from_disconnected_players, - ) - .chain(), - ); - } -} - -/// An event sent when a client is getting disconnected. -#[derive(Event)] -pub struct DisconnectEvent { - pub entity: Entity, - pub reason: Option, -} - -/// A system that removes the several components from our clients when they get -/// a [`DisconnectEvent`]. -pub fn remove_components_from_disconnected_players( - mut commands: Commands, - mut events: EventReader, - mut loaded_by_query: Query<&mut azalea_entity::LoadedBy>, -) { - for DisconnectEvent { entity, .. } in events.read() { - trace!("Got DisconnectEvent for {entity:?}"); - commands - .entity(*entity) - .remove::() - .remove::() - .remove::() - .remove::() - .remove::() - // this makes it close the tcp connection - .remove::() - // swarm detects when this tx gets dropped to fire SwarmEvent::Disconnect - .remove::(); - // note that we don't remove the client from the ECS, so if they decide - // to reconnect they'll keep their state - - // now we have to remove ourselves from the LoadedBy for every entity. - // in theory this could be inefficient if we have massive swarms... but in - // practice this is fine. - for mut loaded_by in &mut loaded_by_query.iter_mut() { - loaded_by.remove(entity); - } - } -} - -#[derive(Component, Clone, Copy, Debug, Deref)] -pub struct IsConnectionAlive(bool); - -fn update_read_packets_task_running_component( - query: Query<(Entity, &RawConnection)>, - mut commands: Commands, -) { - for (entity, raw_connection) in &query { - let running = raw_connection.is_alive(); - commands.entity(entity).insert(IsConnectionAlive(running)); - } -} - -#[allow(clippy::type_complexity)] -fn disconnect_on_connection_dead( - query: Query<(Entity, &IsConnectionAlive), (Changed, With)>, - mut disconnect_events: EventWriter, -) { - for (entity, &is_connection_alive) in &query { - if !*is_connection_alive { - disconnect_events.send(DisconnectEvent { - entity, - reason: None, - }); - } - } -} diff --git a/azalea-client/src/events.rs b/azalea-client/src/events.rs deleted file mode 100644 index aed16bcb..00000000 --- a/azalea-client/src/events.rs +++ /dev/null @@ -1,243 +0,0 @@ -//! Defines the [`Event`] enum and makes those events trigger when they're sent -//! in the ECS. - -use std::sync::Arc; - -use azalea_chat::FormattedText; -use azalea_core::tick::GameTick; -use azalea_protocol::packets::game::{ - ClientboundGamePacket, c_player_combat_kill::ClientboundPlayerCombatKill, -}; -use azalea_world::{InstanceName, MinecraftEntityId}; -use bevy_app::{App, Plugin, PreUpdate, Update}; -use bevy_ecs::{ - component::Component, - event::EventReader, - query::{Added, With}, - schedule::IntoSystemConfigs, - system::Query, -}; -use derive_more::{Deref, DerefMut}; -use tokio::sync::mpsc; - -use crate::{ - PlayerInfo, - chat::{ChatPacket, ChatReceivedEvent}, - disconnect::DisconnectEvent, - packet_handling::game::{ - AddPlayerEvent, DeathEvent, KeepAliveEvent, PacketEvent, RemovePlayerEvent, - UpdatePlayerEvent, - }, -}; - -// (for contributors): -// HOW TO ADD A NEW (packet based) EVENT: -// - make a struct that contains an entity field and a data field (look in -// packet_handling.rs for examples, also you should end the struct name with -// "Event") -// - the entity field is the local player entity that's receiving the event -// - in packet_handling, you always have a variable called player_entity that -// you can use -// - add the event struct in the `impl Plugin for PacketHandlerPlugin` -// - to get the event writer, you have to get an -// EventWriter from the SystemState (the convention is -// to end your variable with the word "events", like "something_events") -// -// - then here in this file, add it to the Event enum -// - and make an event listener system/function like the other ones and put the -// function in the `impl Plugin for EventPlugin` - -/// Something that happened in-game, such as a tick passing or chat message -/// being sent. -/// -/// Note: Events are sent before they're processed, so for example game ticks -/// happen at the beginning of a tick before anything has happened. -#[derive(Debug, Clone)] -pub enum Event { - /// Happens right after the bot switches into the Game state, but before - /// it's actually spawned. This can be useful for setting the client - /// information with `Client::set_client_information`, so the packet - /// doesn't have to be sent twice. - /// - /// You may want to use [`Event::Login`] instead to wait for the bot to be - /// in the world. - Init, - /// The client is now in the world. Fired when we receive a login packet. - Login, - /// A chat message was sent in the game chat. - Chat(ChatPacket), - /// Happens 20 times per second, but only when the world is loaded. - Tick, - /// We received a packet from the server. - /// - /// ``` - /// # use azalea_client::Event; - /// # use azalea_protocol::packets::game::ClientboundGamePacket; - /// # async fn example(event: Event) { - /// # match event { - /// Event::Packet(packet) => match *packet { - /// ClientboundGamePacket::Login(_) => { - /// println!("login packet"); - /// } - /// _ => {} - /// }, - /// # _ => {} - /// # } - /// # } - /// ``` - Packet(Arc), - /// A player joined the game (or more specifically, was added to the tab - /// list). - AddPlayer(PlayerInfo), - /// A player left the game (or maybe is still in the game and was just - /// removed from the tab list). - RemovePlayer(PlayerInfo), - /// A player was updated in the tab list (gamemode, display - /// name, or latency changed). - UpdatePlayer(PlayerInfo), - /// The client player died in-game. - Death(Option>), - /// A `KeepAlive` packet was sent by the server. - KeepAlive(u64), - /// The client disconnected from the server. - Disconnect(Option), -} - -/// A component that contains an event sender for events that are only -/// received by local players. The receiver for this is returned by -/// [`Client::start_client`]. -/// -/// [`Client::start_client`]: crate::Client::start_client -#[derive(Component, Deref, DerefMut)] -pub struct LocalPlayerEvents(pub mpsc::UnboundedSender); - -pub struct EventPlugin; -impl Plugin for EventPlugin { - fn build(&self, app: &mut App) { - app.add_systems( - Update, - ( - chat_listener, - login_listener, - packet_listener, - add_player_listener, - update_player_listener, - remove_player_listener, - keepalive_listener, - death_listener, - disconnect_listener, - ), - ) - .add_systems( - PreUpdate, - init_listener.before(crate::packet_handling::game::process_packet_events), - ) - .add_systems(GameTick, tick_listener); - } -} - -// when LocalPlayerEvents is added, it means the client just started -pub fn init_listener(query: Query<&LocalPlayerEvents, Added>) { - for local_player_events in &query { - let _ = local_player_events.send(Event::Init); - } -} - -// when MinecraftEntityId is added, it means the player is now in the world -pub fn login_listener(query: Query<&LocalPlayerEvents, Added>) { - for local_player_events in &query { - let _ = local_player_events.send(Event::Login); - } -} - -pub fn chat_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader) { - for event in events.read() { - let local_player_events = query - .get(event.entity) - .expect("Non-local entities shouldn't be able to receive chat events"); - let _ = local_player_events.send(Event::Chat(event.packet.clone())); - } -} - -// only tick if we're in a world -pub fn tick_listener(query: Query<&LocalPlayerEvents, With>) { - for local_player_events in &query { - let _ = local_player_events.send(Event::Tick); - } -} - -pub fn packet_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader) { - for event in events.read() { - let local_player_events = query - .get(event.entity) - .expect("Non-local entities shouldn't be able to receive packet events"); - let _ = local_player_events.send(Event::Packet(event.packet.clone())); - } -} - -pub fn add_player_listener( - query: Query<&LocalPlayerEvents>, - mut events: EventReader, -) { - for event in events.read() { - let local_player_events = query - .get(event.entity) - .expect("Non-local entities shouldn't be able to receive add player events"); - let _ = local_player_events.send(Event::AddPlayer(event.info.clone())); - } -} - -pub fn update_player_listener( - query: Query<&LocalPlayerEvents>, - mut events: EventReader, -) { - for event in events.read() { - let local_player_events = query - .get(event.entity) - .expect("Non-local entities shouldn't be able to receive update player events"); - let _ = local_player_events.send(Event::UpdatePlayer(event.info.clone())); - } -} - -pub fn remove_player_listener( - query: Query<&LocalPlayerEvents>, - mut events: EventReader, -) { - for event in events.read() { - let local_player_events = query - .get(event.entity) - .expect("Non-local entities shouldn't be able to receive remove player events"); - let _ = local_player_events.send(Event::RemovePlayer(event.info.clone())); - } -} - -pub fn death_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader) { - for event in events.read() { - if let Ok(local_player_events) = query.get(event.entity) { - let _ = local_player_events.send(Event::Death(event.packet.clone().map(|p| p.into()))); - } - } -} - -pub fn keepalive_listener( - query: Query<&LocalPlayerEvents>, - mut events: EventReader, -) { - for event in events.read() { - let local_player_events = query - .get(event.entity) - .expect("Non-local entities shouldn't be able to receive keepalive events"); - let _ = local_player_events.send(Event::KeepAlive(event.id)); - } -} - -pub fn disconnect_listener( - query: Query<&LocalPlayerEvents>, - mut events: EventReader, -) { - for event in events.read() { - if let Ok(local_player_events) = query.get(event.entity) { - let _ = local_player_events.send(Event::Disconnect(event.reason.clone())); - } - } -} diff --git a/azalea-client/src/interact.rs b/azalea-client/src/interact.rs deleted file mode 100644 index fdeff197..00000000 --- a/azalea-client/src/interact.rs +++ /dev/null @@ -1,369 +0,0 @@ -use std::ops::AddAssign; - -use azalea_block::BlockState; -use azalea_core::{ - block_hit_result::BlockHitResult, - direction::Direction, - game_type::GameMode, - position::{BlockPos, Vec3}, -}; -use azalea_entity::{ - Attributes, EyeHeight, LocalEntity, LookDirection, Position, clamp_look_direction, view_vector, -}; -use azalea_inventory::{ItemStack, ItemStackData, components}; -use azalea_physics::clip::{BlockShapeType, ClipContext, FluidPickType}; -use azalea_protocol::packets::game::{ - s_interact::InteractionHand, - s_swing::ServerboundSwing, - s_use_item_on::{BlockHit, ServerboundUseItemOn}, -}; -use azalea_world::{Instance, InstanceContainer, InstanceName}; -use bevy_app::{App, Plugin, Update}; -use bevy_ecs::{ - component::Component, - entity::Entity, - event::{Event, EventReader, EventWriter}, - query::{Changed, With}, - schedule::IntoSystemConfigs, - system::{Commands, Query, Res}, -}; -use derive_more::{Deref, DerefMut}; -use tracing::warn; - -use crate::{ - Client, - attack::handle_attack_event, - inventory::{Inventory, InventorySet}, - local_player::{LocalGameMode, PermissionLevel, PlayerAbilities}, - movement::MoveEventsSet, - packet_handling::game::{SendPacketEvent, handle_send_packet_event}, - respawn::perform_respawn, -}; - -/// A plugin that allows clients to interact with blocks in the world. -pub struct InteractPlugin; -impl Plugin for InteractPlugin { - fn build(&self, app: &mut App) { - app.add_event::() - .add_event::() - .add_systems( - Update, - ( - ( - update_hit_result_component.after(clamp_look_direction), - handle_block_interact_event, - handle_swing_arm_event, - ) - .before(handle_send_packet_event) - .after(InventorySet) - .after(perform_respawn) - .after(handle_attack_event) - .chain(), - update_modifiers_for_held_item - .after(InventorySet) - .after(MoveEventsSet), - ), - ); - } -} - -impl Client { - /// Right click a block. The behavior of this depends on the target block, - /// and it'll either place the block you're holding in your hand or use the - /// block you clicked (like toggling a lever). - /// - /// Note that this may trigger anticheats as it doesn't take into account - /// whether you're actually looking at the block. - pub fn block_interact(&mut self, position: BlockPos) { - self.ecs.lock().send_event(BlockInteractEvent { - entity: self.entity, - position, - }); - } -} - -/// Right click a block. The behavior of this depends on the target block, -/// and it'll either place the block you're holding in your hand or use the -/// block you clicked (like toggling a lever). -#[derive(Event)] -pub struct BlockInteractEvent { - /// The local player entity that's opening the container. - pub entity: Entity, - /// The coordinates of the container. - pub position: BlockPos, -} - -/// A component that contains the number of changes this client has made to -/// blocks. -#[derive(Component, Copy, Clone, Debug, Default, Deref)] -pub struct CurrentSequenceNumber(u32); - -impl AddAssign 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); - -pub fn handle_block_interact_event( - mut events: EventReader, - mut query: Query<(Entity, &mut CurrentSequenceNumber, &HitResultComponent)>, - mut send_packet_events: EventWriter, -) { - for event in events.read() { - let Ok((entity, mut sequence_number, hit_result)) = query.get_mut(event.entity) else { - warn!("Sent BlockInteractEvent for entity that doesn't have the required components"); - continue; - }; - - // TODO: check to make sure we're within the world border - - *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 - - // the block_hit data will depend on whether we're looking at the block and - // whether we can reach it - - let block_hit = if hit_result.block_pos == event.position { - // we're looking at the block :) - BlockHit { - block_pos: hit_result.block_pos, - direction: hit_result.direction, - location: hit_result.location, - inside: hit_result.inside, - world_border: hit_result.world_border, - } - } else { - // we're not looking at the block, so make up some numbers - BlockHit { - block_pos: event.position, - direction: Direction::Up, - location: event.position.center(), - inside: false, - world_border: false, - } - }; - - send_packet_events.send(SendPacketEvent::new( - entity, - ServerboundUseItemOn { - hand: InteractionHand::MainHand, - block_hit, - sequence: sequence_number.0, - }, - )); - } -} - -#[allow(clippy::type_complexity)] -pub fn update_hit_result_component( - mut commands: Commands, - mut query: Query<( - Entity, - Option<&mut HitResultComponent>, - &LocalGameMode, - &Position, - &EyeHeight, - &LookDirection, - &InstanceName, - )>, - instance_container: Res, -) { - for (entity, hit_result_ref, game_mode, position, eye_height, look_direction, world_name) in - &mut query - { - let pick_range = if game_mode.current == GameMode::Creative { - 6. - } else { - 4.5 - }; - let eye_position = Vec3 { - x: position.x, - y: position.y + **eye_height as f64, - z: position.z, - }; - - let Some(instance_lock) = instance_container.get(world_name) else { - continue; - }; - let instance = instance_lock.read(); - - let hit_result = pick(look_direction, &eye_position, &instance.chunks, pick_range); - if let Some(mut hit_result_ref) = hit_result_ref { - **hit_result_ref = hit_result; - } else { - commands - .entity(entity) - .insert(HitResultComponent(hit_result)); - } - } -} - -/// Get the block that a player would be looking at if their eyes were at the -/// given direction and position. -/// -/// If you need to get the block the player is looking at right now, use -/// [`HitResultComponent`]. -pub fn pick( - look_direction: &LookDirection, - eye_position: &Vec3, - chunks: &azalea_world::ChunkStorage, - pick_range: f64, -) -> BlockHitResult { - let view_vector = view_vector(look_direction); - let end_position = eye_position + &(view_vector * pick_range); - azalea_physics::clip::clip( - chunks, - ClipContext { - from: *eye_position, - to: end_position, - block_shape_type: BlockShapeType::Outline, - fluid_pick_type: FluidPickType::None, - }, - ) -} - -/// Whether we can't interact with the block, based on your gamemode. If -/// this is false, then we can interact with the block. -/// -/// Passing the inventory, block position, and instance is necessary for the -/// adventure mode check. -pub fn check_is_interaction_restricted( - instance: &Instance, - block_pos: &BlockPos, - game_mode: &GameMode, - inventory: &Inventory, -) -> bool { - match game_mode { - GameMode::Adventure => { - // vanilla checks for abilities.mayBuild here but servers have no - // way of modifying that - - let held_item = inventory.held_item(); - match &held_item { - ItemStack::Present(item) => { - let block = instance.chunks.get_block_state(block_pos); - let Some(block) = block else { - // block isn't loaded so just say that it is restricted - return true; - }; - check_block_can_be_broken_by_item_in_adventure_mode(item, &block) - } - _ => true, - } - } - GameMode::Spectator => true, - _ => false, - } -} - -/// Check if the item has the `CanDestroy` tag for the block. -pub fn check_block_can_be_broken_by_item_in_adventure_mode( - item: &ItemStackData, - _block: &BlockState, -) -> bool { - // minecraft caches the last checked block but that's kind of an unnecessary - // optimization and makes the code too complicated - - if !item.components.has::() { - // no CanDestroy tag - return false; - }; - - false - - // for block_predicate in can_destroy { - // // TODO - // // defined in BlockPredicateArgument.java - // } - - // 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, -} -pub fn handle_swing_arm_event( - mut events: EventReader, - mut send_packet_events: EventWriter, -) { - for event in events.read() { - send_packet_events.send(SendPacketEvent::new( - event.entity, - ServerboundSwing { - hand: InteractionHand::MainHand, - }, - )); - } -} - -#[allow(clippy::type_complexity)] -fn update_modifiers_for_held_item( - mut query: Query<(&mut Attributes, &Inventory), (With, Changed)>, -) { - for (mut attributes, inventory) in &mut query { - let held_item = inventory.held_item(); - - use azalea_registry::Item; - let added_attack_speed = match held_item.kind() { - Item::WoodenSword => -2.4, - Item::WoodenShovel => -3.0, - Item::WoodenPickaxe => -2.8, - Item::WoodenAxe => -3.2, - Item::WoodenHoe => -3.0, - - Item::StoneSword => -2.4, - Item::StoneShovel => -3.0, - Item::StonePickaxe => -2.8, - Item::StoneAxe => -3.2, - Item::StoneHoe => -2.0, - - Item::GoldenSword => -2.4, - Item::GoldenShovel => -3.0, - Item::GoldenPickaxe => -2.8, - Item::GoldenAxe => -3.0, - Item::GoldenHoe => -3.0, - - Item::IronSword => -2.4, - Item::IronShovel => -3.0, - Item::IronPickaxe => -2.8, - Item::IronAxe => -3.1, - Item::IronHoe => -1.0, - - Item::DiamondSword => -2.4, - Item::DiamondShovel => -3.0, - Item::DiamondPickaxe => -2.8, - Item::DiamondAxe => -3.0, - Item::DiamondHoe => 0.0, - - Item::NetheriteSword => -2.4, - Item::NetheriteShovel => -3.0, - Item::NetheritePickaxe => -2.8, - Item::NetheriteAxe => -3.0, - Item::NetheriteHoe => 0.0, - - Item::Trident => -2.9, - _ => 0., - }; - attributes - .attack_speed - .insert(azalea_entity::attributes::base_attack_speed_modifier( - added_attack_speed, - )); - } -} diff --git a/azalea-client/src/inventory.rs b/azalea-client/src/inventory.rs deleted file mode 100644 index 4d796c9c..00000000 --- a/azalea-client/src/inventory.rs +++ /dev/null @@ -1,768 +0,0 @@ -use std::collections::{HashMap, HashSet}; - -use azalea_chat::FormattedText; -pub use azalea_inventory::*; -use azalea_inventory::{ - item::MaxStackSizeExt, - operations::{ - ClickOperation, CloneClick, PickupAllClick, PickupClick, QuickCraftKind, QuickCraftStatus, - QuickCraftStatusKind, QuickMoveClick, ThrowClick, - }, -}; -use azalea_protocol::packets::game::{ - s_container_click::ServerboundContainerClick, s_container_close::ServerboundContainerClose, - s_set_carried_item::ServerboundSetCarriedItem, -}; -use azalea_registry::MenuKind; -use bevy_app::{App, Plugin, Update}; -use bevy_ecs::{ - component::Component, - entity::Entity, - event::EventReader, - prelude::{Event, EventWriter}, - schedule::{IntoSystemConfigs, SystemSet}, - system::Query, -}; -use tracing::warn; - -use crate::{ - Client, - local_player::PlayerAbilities, - packet_handling::game::{SendPacketEvent, handle_send_packet_event}, - respawn::perform_respawn, -}; - -pub struct InventoryPlugin; -impl Plugin for InventoryPlugin { - fn build(&self, app: &mut App) { - app.add_event::() - .add_event::() - .add_event::() - .add_event::() - .add_event::() - .add_event::() - .add_systems( - Update, - ( - handle_set_selected_hotbar_slot_event, - handle_menu_opened_event, - handle_set_container_content_event, - handle_container_click_event, - handle_container_close_event.before(handle_send_packet_event), - handle_client_side_close_container_event, - ) - .chain() - .in_set(InventorySet) - .before(perform_respawn), - ); - } -} - -#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)] -pub struct InventorySet; - -impl Client { - /// Return the menu that is currently open. If no menu is open, this will - /// have the player's inventory. - pub fn menu(&self) -> Menu { - let mut ecs = self.ecs.lock(); - let inventory = self.query::<&Inventory>(&mut ecs); - inventory.menu().clone() - } -} - -/// A component present on all local players that have an inventory. -#[derive(Component, Debug, Clone)] -pub struct Inventory { - /// A component that contains the player's inventory menu. This is - /// guaranteed to be a `Menu::Player`. - /// - /// We keep it as a [`Menu`] since `Menu` has some useful functions that - /// bare [`azalea_inventory::Player`] doesn't have. - pub inventory_menu: azalea_inventory::Menu, - - /// The ID of the container that's currently open. Its value is not - /// guaranteed to be anything specific, and may change every time you open a - /// container (unless it's 0, in which case it means that no container is - /// open). - pub id: i32, - /// The current container menu that the player has open. If no container is - /// open, this will be `None`. - pub container_menu: Option, - /// The custom name of the menu that's currently open. This is Some when - /// `container_menu` is Some. - pub container_menu_title: Option, - /// The item that is currently held by the cursor. `Slot::Empty` if nothing - /// is currently being held. - /// - /// This is different from [`Self::selected_hotbar_slot`], which is the - /// item that's selected in the hotbar. - pub carried: ItemStack, - /// An identifier used by the server to track client inventory desyncs. This - /// is sent on every container click, and it's only ever updated when the - /// server sends a new container update. - pub state_id: u32, - - pub quick_craft_status: QuickCraftStatusKind, - pub quick_craft_kind: QuickCraftKind, - /// A set of the indexes of the slots that have been right clicked in - /// this "quick craft". - pub quick_craft_slots: HashSet, - - /// The index of the item in the hotbar that's currently being held by the - /// player. This MUST be in the range 0..9 (not including 9). - /// - /// In a vanilla client this is changed by pressing the number keys or using - /// the scroll wheel. - pub selected_hotbar_slot: u8, -} - -impl Inventory { - /// Returns a reference to the currently active menu. If a container is open - /// it'll return [`Self::container_menu`], otherwise - /// [`Self::inventory_menu`]. - /// - /// Use [`Self::menu_mut`] if you need a mutable reference. - pub fn menu(&self) -> &azalea_inventory::Menu { - match &self.container_menu { - Some(menu) => menu, - _ => &self.inventory_menu, - } - } - - /// Returns a mutable reference to the currently active menu. If a container - /// is open it'll return [`Self::container_menu`], otherwise - /// [`Self::inventory_menu`]. - /// - /// Use [`Self::menu`] if you don't need a mutable reference. - pub fn menu_mut(&mut self) -> &mut azalea_inventory::Menu { - match &mut self.container_menu { - Some(menu) => menu, - _ => &mut self.inventory_menu, - } - } - - /// Modify the inventory as if the given operation was performed on it. - pub fn simulate_click( - &mut self, - operation: &ClickOperation, - player_abilities: &PlayerAbilities, - ) { - if let ClickOperation::QuickCraft(quick_craft) = operation { - let last_quick_craft_status_tmp = self.quick_craft_status.clone(); - self.quick_craft_status = last_quick_craft_status_tmp.clone(); - let last_quick_craft_status = last_quick_craft_status_tmp; - - // no carried item, reset - if self.carried.is_empty() { - return self.reset_quick_craft(); - } - // if we were starting or ending, or now we aren't ending and the status - // changed, reset - if (last_quick_craft_status == QuickCraftStatusKind::Start - || last_quick_craft_status == QuickCraftStatusKind::End - || self.quick_craft_status != QuickCraftStatusKind::End) - && (self.quick_craft_status != last_quick_craft_status) - { - return self.reset_quick_craft(); - } - if self.quick_craft_status == QuickCraftStatusKind::Start { - self.quick_craft_kind = quick_craft.kind.clone(); - if self.quick_craft_kind == QuickCraftKind::Middle && player_abilities.instant_break - { - self.quick_craft_status = QuickCraftStatusKind::Add; - self.quick_craft_slots.clear(); - } else { - self.reset_quick_craft(); - } - return; - } - if let QuickCraftStatus::Add { slot } = quick_craft.status { - let slot_item = self.menu().slot(slot as usize); - if let Some(slot_item) = slot_item { - if let ItemStack::Present(carried) = &self.carried { - // minecraft also checks slot.may_place(carried) and - // menu.can_drag_to(slot) - // but they always return true so they're not relevant for us - if can_item_quick_replace(slot_item, &self.carried, true) - && (self.quick_craft_kind == QuickCraftKind::Right - || carried.count as usize > self.quick_craft_slots.len()) - { - self.quick_craft_slots.insert(slot); - } - } - } - return; - } - if self.quick_craft_status == QuickCraftStatusKind::End { - if !self.quick_craft_slots.is_empty() { - if self.quick_craft_slots.len() == 1 { - // if we only clicked one slot, then turn this - // QuickCraftClick into a PickupClick - let slot = *self.quick_craft_slots.iter().next().unwrap(); - self.reset_quick_craft(); - self.simulate_click( - &match self.quick_craft_kind { - QuickCraftKind::Left => { - PickupClick::Left { slot: Some(slot) }.into() - } - QuickCraftKind::Right => { - PickupClick::Left { slot: Some(slot) }.into() - } - QuickCraftKind::Middle => { - // idk just do nothing i guess - return; - } - }, - player_abilities, - ); - return; - } - - let ItemStack::Present(mut carried) = self.carried.clone() else { - // this should never happen - return self.reset_quick_craft(); - }; - - let mut carried_count = carried.count; - let mut quick_craft_slots_iter = self.quick_craft_slots.iter(); - - loop { - let mut slot: &ItemStack; - let mut slot_index: u16; - let mut item_stack: &ItemStack; - - loop { - let Some(&next_slot) = quick_craft_slots_iter.next() else { - carried.count = carried_count; - self.carried = ItemStack::Present(carried); - return self.reset_quick_craft(); - }; - - slot = self.menu().slot(next_slot as usize).unwrap(); - slot_index = next_slot; - item_stack = &self.carried; - - if slot.is_present() - && can_item_quick_replace(slot, item_stack, true) - // this always returns true in most cases - // && slot.may_place(item_stack) - && ( - self.quick_craft_kind == QuickCraftKind::Middle - || item_stack.count() >= self.quick_craft_slots.len() as i32 - ) - { - break; - } - } - - // get the ItemStackData for the slot - let ItemStack::Present(slot) = slot else { - unreachable!("the loop above requires the slot to be present to break") - }; - - // if self.can_drag_to(slot) { - let mut new_carried = carried.clone(); - let slot_item_count = slot.count; - get_quick_craft_slot_count( - &self.quick_craft_slots, - &self.quick_craft_kind, - &mut new_carried, - slot_item_count, - ); - let max_stack_size = i32::min( - new_carried.kind.max_stack_size(), - i32::min( - new_carried.kind.max_stack_size(), - slot.kind.max_stack_size(), - ), - ); - if new_carried.count > max_stack_size { - new_carried.count = max_stack_size; - } - - carried_count -= new_carried.count - slot_item_count; - // we have to inline self.menu_mut() here to avoid the borrow checker - // complaining - let menu = match &mut self.container_menu { - Some(menu) => menu, - _ => &mut self.inventory_menu, - }; - *menu.slot_mut(slot_index as usize).unwrap() = - ItemStack::Present(new_carried); - } - } - } else { - return self.reset_quick_craft(); - } - } - // the quick craft status should always be in start if we're not in quick craft - // mode - if self.quick_craft_status != QuickCraftStatusKind::Start { - return self.reset_quick_craft(); - } - - match operation { - // left clicking outside inventory - ClickOperation::Pickup(PickupClick::Left { slot: None }) => { - if self.carried.is_present() { - // vanilla has `player.drop`s but they're only used - // server-side - // they're included as comments here in case you want to adapt this for a server - // implementation - - // player.drop(self.carried, true); - self.carried = ItemStack::Empty; - } - } - ClickOperation::Pickup(PickupClick::Right { slot: None }) => { - if self.carried.is_present() { - let _item = self.carried.split(1); - // player.drop(item, true); - } - } - ClickOperation::Pickup( - PickupClick::Left { slot: Some(slot) } | PickupClick::Right { slot: Some(slot) }, - ) => { - let Some(slot_item) = self.menu().slot(*slot as usize) else { - return; - }; - let carried = &self.carried; - // vanilla does a check called tryItemClickBehaviourOverride - // here - // i don't understand it so i didn't implement it - match slot_item { - ItemStack::Empty => if carried.is_present() {}, - ItemStack::Present(_) => todo!(), - } - } - ClickOperation::QuickMove( - QuickMoveClick::Left { slot } | QuickMoveClick::Right { slot }, - ) => { - // in vanilla it also tests if QuickMove has a slot index of -999 - // but i don't think that's ever possible so it's not covered here - loop { - let new_slot_item = self.menu_mut().quick_move_stack(*slot as usize); - let slot_item = self.menu().slot(*slot as usize).unwrap(); - if new_slot_item.is_empty() || slot_item != &new_slot_item { - break; - } - } - } - ClickOperation::Swap(s) => { - let source_slot_index = s.source_slot as usize; - let target_slot_index = s.target_slot as usize; - - let Some(source_slot) = self.menu().slot(source_slot_index) else { - return; - }; - let Some(target_slot) = self.menu().slot(target_slot_index) else { - return; - }; - if source_slot.is_empty() && target_slot.is_empty() { - return; - } - - if target_slot.is_empty() { - if self.menu().may_pickup(source_slot_index) { - let source_slot = source_slot.clone(); - let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap(); - *target_slot = source_slot; - } - } else if source_slot.is_empty() { - let ItemStack::Present(target_item) = target_slot else { - unreachable!("target slot is not empty but is not present"); - }; - if self.menu().may_place(source_slot_index, target_item) { - // get the target_item but mutable - let source_max_stack_size = self.menu().max_stack_size(source_slot_index); - - let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap(); - let new_source_slot = target_slot.split(source_max_stack_size); - *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot; - } - } else if self.menu().may_pickup(source_slot_index) { - let ItemStack::Present(target_item) = target_slot else { - unreachable!("target slot is not empty but is not present"); - }; - if self.menu().may_place(source_slot_index, target_item) { - let source_max_stack = self.menu().max_stack_size(source_slot_index); - if target_slot.count() > source_max_stack as i32 { - // if there's more than the max stack size in the target slot - - let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap(); - let new_source_slot = target_slot.split(source_max_stack); - *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot; - // if !self.inventory_menu.add(new_source_slot) { - // player.drop(new_source_slot, true); - // } - } else { - // normal swap - let new_target_slot = source_slot.clone(); - let new_source_slot = target_slot.clone(); - - let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap(); - *target_slot = new_target_slot; - - let source_slot = self.menu_mut().slot_mut(source_slot_index).unwrap(); - *source_slot = new_source_slot; - } - } - } - } - ClickOperation::Clone(CloneClick { slot }) => { - if !player_abilities.instant_break || self.carried.is_present() { - return; - } - let Some(source_slot) = self.menu().slot(*slot as usize) else { - return; - }; - let ItemStack::Present(source_item) = source_slot else { - return; - }; - let mut new_carried = source_item.clone(); - new_carried.count = new_carried.kind.max_stack_size(); - self.carried = ItemStack::Present(new_carried); - } - ClickOperation::Throw(c) => { - if self.carried.is_present() { - return; - } - - let (ThrowClick::Single { slot: slot_index } - | ThrowClick::All { slot: slot_index }) = c; - let slot_index = *slot_index as usize; - - let Some(slot) = self.menu_mut().slot_mut(slot_index) else { - return; - }; - let ItemStack::Present(slot_item) = slot else { - return; - }; - - let dropping_count = match c { - ThrowClick::Single { .. } => 1, - ThrowClick::All { .. } => slot_item.count, - }; - - let _dropping = slot_item.split(dropping_count as u32); - // player.drop(dropping, true); - } - ClickOperation::PickupAll(PickupAllClick { - slot: source_slot_index, - reversed, - }) => { - let source_slot_index = *source_slot_index as usize; - - let source_slot = self.menu().slot(source_slot_index).unwrap(); - let target_slot = self.carried.clone(); - - if target_slot.is_empty() - || (source_slot.is_present() && self.menu().may_pickup(source_slot_index)) - { - return; - } - - let ItemStack::Present(target_slot_item) = &target_slot else { - unreachable!("target slot is not empty but is not present"); - }; - - for round in 0..2 { - let iterator: Box> = if *reversed { - Box::new((0..self.menu().len()).rev()) - } else { - Box::new(0..self.menu().len()) - }; - - for i in iterator { - if target_slot_item.count < target_slot_item.kind.max_stack_size() { - let checking_slot = self.menu().slot(i).unwrap(); - if let ItemStack::Present(checking_item) = checking_slot { - if can_item_quick_replace(checking_slot, &target_slot, true) - && self.menu().may_pickup(i) - && (round != 0 - || checking_item.count - != checking_item.kind.max_stack_size()) - { - // get the checking_slot and checking_item again but mutable - let checking_slot = self.menu_mut().slot_mut(i).unwrap(); - - let taken_item = - checking_slot.split(checking_slot.count() as u32); - - // now extend the carried item - let target_slot = &mut self.carried; - let ItemStack::Present(target_slot_item) = target_slot else { - unreachable!("target slot is not empty but is not present"); - }; - target_slot_item.count += taken_item.count(); - } - } - } - } - } - } - _ => {} - } - } - - fn reset_quick_craft(&mut self) { - self.quick_craft_status = QuickCraftStatusKind::Start; - self.quick_craft_slots.clear(); - } - - /// Get the item in the player's hotbar that is currently being held. - pub fn held_item(&self) -> ItemStack { - let inventory = &self.inventory_menu; - let hotbar_items = &inventory.slots()[inventory.hotbar_slots_range()]; - hotbar_items[self.selected_hotbar_slot as usize].clone() - } -} - -fn can_item_quick_replace( - target_slot: &ItemStack, - item: &ItemStack, - ignore_item_count: bool, -) -> bool { - let ItemStack::Present(target_slot) = target_slot else { - return false; - }; - let ItemStack::Present(item) = item else { - // i *think* this is what vanilla does - // not 100% sure lol probably doesn't matter though - return false; - }; - - if !item.is_same_item_and_components(target_slot) { - return false; - } - let count = target_slot.count as u16 - + if ignore_item_count { - 0 - } else { - item.count as u16 - }; - count <= item.kind.max_stack_size() as u16 -} - -fn get_quick_craft_slot_count( - quick_craft_slots: &HashSet, - quick_craft_kind: &QuickCraftKind, - item: &mut ItemStackData, - slot_item_count: i32, -) { - item.count = match quick_craft_kind { - QuickCraftKind::Left => item.count / quick_craft_slots.len() as i32, - QuickCraftKind::Right => 1, - QuickCraftKind::Middle => item.kind.max_stack_size(), - }; - item.count += slot_item_count; -} - -impl Default for Inventory { - fn default() -> Self { - Inventory { - inventory_menu: Menu::Player(azalea_inventory::Player::default()), - id: 0, - container_menu: None, - container_menu_title: None, - carried: ItemStack::Empty, - state_id: 0, - quick_craft_status: QuickCraftStatusKind::Start, - quick_craft_kind: QuickCraftKind::Middle, - quick_craft_slots: HashSet::new(), - selected_hotbar_slot: 0, - } - } -} - -/// Sent from the server when a menu (like a chest or crafting table) was -/// opened by the client. -#[derive(Event, Debug)] -pub struct MenuOpenedEvent { - pub entity: Entity, - pub window_id: i32, - pub menu_type: MenuKind, - pub title: FormattedText, -} -fn handle_menu_opened_event( - mut events: EventReader, - mut query: Query<&mut Inventory>, -) { - for event in events.read() { - let mut inventory = query.get_mut(event.entity).unwrap(); - inventory.id = event.window_id; - inventory.container_menu = Some(Menu::from_kind(event.menu_type)); - inventory.container_menu_title = Some(event.title.clone()); - } -} - -/// Tell the server that we want to close a container. -/// -/// Note that this is also sent when the client closes its own inventory, even -/// though there is no packet for opening its inventory. -#[derive(Event)] -pub struct CloseContainerEvent { - pub entity: Entity, - /// The ID of the container to close. 0 for the player's inventory. If this - /// is not the same as the currently open inventory, nothing will happen. - pub id: i32, -} -fn handle_container_close_event( - query: Query<(Entity, &Inventory)>, - mut events: EventReader, - mut client_side_events: EventWriter, - mut send_packet_events: EventWriter, -) { - for event in events.read() { - let (entity, inventory) = query.get(event.entity).unwrap(); - if event.id != inventory.id { - warn!( - "Tried to close container with ID {}, but the current container ID is {}", - event.id, inventory.id - ); - continue; - } - - send_packet_events.send(SendPacketEvent::new( - entity, - ServerboundContainerClose { - container_id: inventory.id, - }, - )); - client_side_events.send(ClientSideCloseContainerEvent { - entity: event.entity, - }); - } -} - -/// Close a container without notifying the server. -/// -/// Note that this also gets fired when we get a [`CloseContainerEvent`]. -#[derive(Event)] -pub struct ClientSideCloseContainerEvent { - pub entity: Entity, -} -pub fn handle_client_side_close_container_event( - mut events: EventReader, - mut query: Query<&mut Inventory>, -) { - for event in events.read() { - let mut inventory = query.get_mut(event.entity).unwrap(); - inventory.container_menu = None; - inventory.id = 0; - inventory.container_menu_title = None; - } -} - -#[derive(Event, Debug)] -pub struct ContainerClickEvent { - pub entity: Entity, - pub window_id: i32, - pub operation: ClickOperation, -} -pub fn handle_container_click_event( - mut query: Query<(Entity, &mut Inventory)>, - mut events: EventReader, - mut send_packet_events: EventWriter, -) { - for event in events.read() { - let (entity, mut inventory) = query.get_mut(event.entity).unwrap(); - if inventory.id != event.window_id { - warn!( - "Tried to click container with ID {}, but the current container ID is {}", - event.window_id, inventory.id - ); - continue; - } - - let menu = inventory.menu_mut(); - let old_slots = menu.slots().clone(); - - // menu.click(&event.operation); - - // see which slots changed after clicking and put them in the hashmap - // the server uses this to check if we desynced - let mut changed_slots: HashMap = HashMap::new(); - for (slot_index, old_slot) in old_slots.iter().enumerate() { - let new_slot = &menu.slots()[slot_index]; - if old_slot != new_slot { - changed_slots.insert(slot_index as u16, new_slot.clone()); - } - } - - send_packet_events.send(SendPacketEvent::new( - entity, - ServerboundContainerClick { - container_id: event.window_id, - state_id: inventory.state_id, - slot_num: event.operation.slot_num().map(|n| n as i16).unwrap_or(-999), - button_num: event.operation.button_num(), - click_type: event.operation.click_type(), - changed_slots, - carried_item: inventory.carried.clone(), - }, - )); - } -} - -/// Sent from the server when the contents of a container are replaced. Usually -/// triggered by the `ContainerSetContent` packet. -#[derive(Event)] -pub struct SetContainerContentEvent { - pub entity: Entity, - pub slots: Vec, - pub container_id: i32, -} -fn handle_set_container_content_event( - mut events: EventReader, - mut query: Query<&mut Inventory>, -) { - for event in events.read() { - let mut inventory = query.get_mut(event.entity).unwrap(); - - if event.container_id != inventory.id { - warn!( - "Tried to set container content with ID {}, but the current container ID is {}", - event.container_id, inventory.id - ); - continue; - } - - let menu = inventory.menu_mut(); - for (i, slot) in event.slots.iter().enumerate() { - if let Some(slot_mut) = menu.slot_mut(i) { - *slot_mut = slot.clone(); - } - } - } -} - -#[derive(Event)] -pub struct SetSelectedHotbarSlotEvent { - pub entity: Entity, - /// The hotbar slot to select. This should be in the range 0..=8. - pub slot: u8, -} -fn handle_set_selected_hotbar_slot_event( - mut events: EventReader, - mut send_packet_events: EventWriter, - mut query: Query<&mut Inventory>, -) { - for event in events.read() { - let mut inventory = query.get_mut(event.entity).unwrap(); - - // if the slot is already selected, don't send a packet - if inventory.selected_hotbar_slot == event.slot { - continue; - } - - inventory.selected_hotbar_slot = event.slot; - send_packet_events.send(SendPacketEvent::new( - event.entity, - ServerboundSetCarriedItem { - slot: event.slot as u16, - }, - )); - } -} diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs index abe7c692..d2302b78 100644 --- a/azalea-client/src/lib.rs +++ b/azalea-client/src/lib.rs @@ -8,26 +8,13 @@ #![feature(error_generic_member_access)] mod account; -pub mod attack; -pub mod chat; -pub mod chunks; mod client; -pub mod configuration; -pub mod disconnect; mod entity_query; -pub mod events; -pub mod interact; -pub mod inventory; mod local_player; -pub mod mining; -pub mod movement; -pub mod packet_handling; pub mod ping; mod player; +mod plugins; pub mod raw_connection; -pub mod respawn; -pub mod send_client_end; -pub mod task_pool; #[doc(hidden)] pub mod test_simulation; @@ -44,3 +31,4 @@ pub use movement::{ PhysicsState, SprintDirection, StartSprintEvent, StartWalkEvent, WalkDirection, }; pub use player::PlayerInfo; +pub use plugins::*; diff --git a/azalea-client/src/local_player.rs b/azalea-client/src/local_player.rs index 7e323f4c..455cc470 100644 --- a/azalea-client/src/local_player.rs +++ b/azalea-client/src/local_player.rs @@ -2,7 +2,6 @@ use std::{collections::HashMap, io, sync::Arc}; use azalea_auth::game_profile::GameProfile; use azalea_core::game_type::GameMode; -use azalea_entity::Dead; use azalea_protocol::packets::game::c_player_abilities::ClientboundPlayerAbilities; use azalea_world::{Instance, PartialInstance}; use bevy_ecs::{component::Component, prelude::*}; @@ -13,10 +12,7 @@ use tokio::sync::mpsc; use tracing::error; use uuid::Uuid; -use crate::{ - ClientInformation, PlayerInfo, - events::{Event as AzaleaEvent, LocalPlayerEvents}, -}; +use crate::{ClientInformation, PlayerInfo, events::Event as AzaleaEvent}; /// A component that keeps strong references to our [`PartialInstance`] and /// [`Instance`] for local players. @@ -150,13 +146,6 @@ impl InstanceHolder { } } -/// Send the "Death" event for [`LocalEntity`]s that died with no reason. -pub fn death_event(query: Query<&LocalPlayerEvents, Added>) { - for local_player_events in &query { - local_player_events.send(AzaleaEvent::Death(None)).unwrap(); - } -} - #[derive(Error, Debug)] pub enum HandlePacketError { #[error("{0}")] diff --git a/azalea-client/src/mining.rs b/azalea-client/src/mining.rs deleted file mode 100644 index 03063b3e..00000000 --- a/azalea-client/src/mining.rs +++ /dev/null @@ -1,643 +0,0 @@ -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_handling::game::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::() - .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 }); - } -} diff --git a/azalea-client/src/movement.rs b/azalea-client/src/movement.rs deleted file mode 100644 index b0ff70f4..00000000 --- a/azalea-client/src/movement.rs +++ /dev/null @@ -1,580 +0,0 @@ -use std::backtrace::Backtrace; - -use azalea_core::position::Vec3; -use azalea_core::tick::GameTick; -use azalea_entity::{Attributes, Jumping, metadata::Sprinting}; -use azalea_entity::{InLoadedChunk, LastSentPosition, LookDirection, Physics, Position}; -use azalea_physics::{PhysicsSet, ai_step}; -use azalea_protocol::packets::game::{ServerboundPlayerCommand, ServerboundPlayerInput}; -use azalea_protocol::packets::{ - Packet, - game::{ - s_move_player_pos::ServerboundMovePlayerPos, - s_move_player_pos_rot::ServerboundMovePlayerPosRot, - s_move_player_rot::ServerboundMovePlayerRot, - s_move_player_status_only::ServerboundMovePlayerStatusOnly, - }, -}; -use azalea_world::{MinecraftEntityId, MoveEntityError}; -use bevy_app::{App, Plugin, Update}; -use bevy_ecs::prelude::{Event, EventWriter}; -use bevy_ecs::schedule::SystemSet; -use bevy_ecs::system::Commands; -use bevy_ecs::{ - component::Component, entity::Entity, event::EventReader, query::With, - schedule::IntoSystemConfigs, system::Query, -}; -use thiserror::Error; - -use crate::client::Client; -use crate::packet_handling::game::SendPacketEvent; - -#[derive(Error, Debug)] -pub enum MovePlayerError { - #[error("Player is not in world")] - PlayerNotInWorld(Backtrace), - #[error("{0}")] - Io(#[from] std::io::Error), -} - -impl From for MovePlayerError { - fn from(err: MoveEntityError) -> Self { - match err { - MoveEntityError::EntityDoesNotExist(backtrace) => { - MovePlayerError::PlayerNotInWorld(backtrace) - } - } - } -} - -pub struct PlayerMovePlugin; - -impl Plugin for PlayerMovePlugin { - fn build(&self, app: &mut App) { - app.add_event::() - .add_event::() - .add_event::() - .add_systems( - Update, - (handle_sprint, handle_walk, handle_knockback) - .chain() - .in_set(MoveEventsSet), - ) - .add_systems( - GameTick, - ( - (tick_controls, local_player_ai_step) - .chain() - .in_set(PhysicsSet) - .before(ai_step) - .before(azalea_physics::fluids::update_in_water_state_and_do_fluid_pushing), - send_sprinting_if_needed.after(azalea_entity::update_in_loaded_chunk), - send_player_input_packet, - send_position.after(PhysicsSet), - ) - .chain(), - ); - } -} - -#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)] -pub struct MoveEventsSet; - -impl Client { - /// Set whether we're jumping. This acts as if you held space in - /// vanilla. If you want to jump once, use the `jump` function. - /// - /// If you're making a realistic client, calling this function every tick is - /// recommended. - pub fn set_jumping(&mut self, jumping: bool) { - let mut ecs = self.ecs.lock(); - let mut jumping_mut = self.query::<&mut Jumping>(&mut ecs); - **jumping_mut = jumping; - } - - /// Returns whether the player will try to jump next tick. - pub fn jumping(&self) -> bool { - *self.component::() - } - - /// Sets the direction the client is looking. `y_rot` is yaw (looking to the - /// side), `x_rot` is pitch (looking up and down). You can get these - /// numbers from the vanilla f3 screen. - /// `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 LookDirection>(&mut ecs); - - (look_direction.y_rot, look_direction.x_rot) = (y_rot, x_rot); - } - - /// Returns the direction the client is looking. The first value is the y - /// rotation (ie. yaw, looking to the side) and the second value is the x - /// rotation (ie. pitch, looking up and down). - pub fn direction(&self) -> (f32, f32) { - let look_direction = self.component::(); - (look_direction.y_rot, look_direction.x_rot) - } -} - -/// A component that contains the look direction that was last sent over the -/// network. -#[derive(Debug, Component, Clone, Default)] -pub struct LastSentLookDirection { - pub x_rot: f32, - pub y_rot: f32, -} - -/// Component for entities that can move and sprint. Usually only in -/// [`LocalEntity`]s. -/// -/// [`LocalEntity`]: azalea_entity::LocalEntity -#[derive(Default, Component, Clone)] -pub struct PhysicsState { - /// Minecraft only sends a movement packet either after 20 ticks or if the - /// player moved enough. This is that tick counter. - pub position_remainder: u32, - pub was_sprinting: bool, - // Whether we're going to try to start sprinting this tick. Equivalent to - // holding down ctrl for a tick. - pub trying_to_sprint: bool, - - pub move_direction: WalkDirection, - pub forward_impulse: f32, - pub left_impulse: f32, -} - -#[allow(clippy::type_complexity)] -pub fn send_position( - mut query: Query< - ( - Entity, - &Position, - &LookDirection, - &mut PhysicsState, - &mut LastSentPosition, - &mut Physics, - &mut LastSentLookDirection, - ), - With, - >, - mut send_packet_events: EventWriter, -) { - for ( - entity, - position, - direction, - mut physics_state, - mut last_sent_position, - mut physics, - mut last_direction, - ) in query.iter_mut() - { - let packet = { - // TODO: the camera being able to be controlled by other entities isn't - // implemented yet if !self.is_controlled_camera() { return }; - - let x_delta = position.x - last_sent_position.x; - let y_delta = position.y - last_sent_position.y; - let z_delta = position.z - last_sent_position.z; - let y_rot_delta = (direction.y_rot - last_direction.y_rot) as f64; - let x_rot_delta = (direction.x_rot - last_direction.x_rot) as f64; - - physics_state.position_remainder += 1; - - // boolean sendingPosition = Mth.lengthSquared(xDelta, yDelta, zDelta) > - // Mth.square(2.0E-4D) || this.positionReminder >= 20; - let sending_position = ((x_delta.powi(2) + y_delta.powi(2) + z_delta.powi(2)) - > 2.0e-4f64.powi(2)) - || physics_state.position_remainder >= 20; - let sending_direction = y_rot_delta != 0.0 || x_rot_delta != 0.0; - - // if self.is_passenger() { - // TODO: posrot packet for being a passenger - // } - let packet = if sending_position && sending_direction { - Some( - ServerboundMovePlayerPosRot { - pos: **position, - look_direction: *direction, - on_ground: physics.on_ground(), - } - .into_variant(), - ) - } else if sending_position { - Some( - ServerboundMovePlayerPos { - pos: **position, - on_ground: physics.on_ground(), - } - .into_variant(), - ) - } else if sending_direction { - Some( - ServerboundMovePlayerRot { - look_direction: *direction, - on_ground: physics.on_ground(), - } - .into_variant(), - ) - } else if physics.last_on_ground() != physics.on_ground() { - Some( - ServerboundMovePlayerStatusOnly { - on_ground: physics.on_ground(), - } - .into_variant(), - ) - } else { - None - }; - - if sending_position { - **last_sent_position = **position; - physics_state.position_remainder = 0; - } - if sending_direction { - last_direction.y_rot = direction.y_rot; - last_direction.x_rot = direction.x_rot; - } - - let on_ground = physics.on_ground(); - physics.set_last_on_ground(on_ground); - // minecraft checks for autojump here, but also autojump is bad so - - packet - }; - - if let Some(packet) = packet { - send_packet_events.send(SendPacketEvent { - sent_by: entity, - packet, - }); - } - } -} - -#[derive(Debug, Default, Component, Clone, PartialEq, Eq)] -pub struct LastSentInput(pub ServerboundPlayerInput); -pub fn send_player_input_packet( - mut query: Query<(Entity, &PhysicsState, &Jumping, Option<&LastSentInput>)>, - mut send_packet_events: EventWriter, - mut commands: Commands, -) { - for (entity, physics_state, jumping, last_sent_input) in query.iter_mut() { - let dir = physics_state.move_direction; - type D = WalkDirection; - let input = ServerboundPlayerInput { - forward: matches!(dir, D::Forward | D::ForwardLeft | D::ForwardRight), - backward: matches!(dir, D::Backward | D::BackwardLeft | D::BackwardRight), - left: matches!(dir, D::Left | D::ForwardLeft | D::BackwardLeft), - right: matches!(dir, D::Right | D::ForwardRight | D::BackwardRight), - jump: **jumping, - // TODO: implement sneaking - shift: false, - sprint: physics_state.trying_to_sprint, - }; - - // if LastSentInput isn't present, we default to assuming we're not pressing any - // keys and insert it anyways every time it changes - let last_sent_input = last_sent_input.cloned().unwrap_or_default(); - - if input != last_sent_input.0 { - send_packet_events.send(SendPacketEvent { - sent_by: entity, - packet: input.clone().into_variant(), - }); - commands.entity(entity).insert(LastSentInput(input)); - } - } -} - -fn send_sprinting_if_needed( - mut query: Query<(Entity, &MinecraftEntityId, &Sprinting, &mut PhysicsState)>, - mut send_packet_events: EventWriter, -) { - for (entity, minecraft_entity_id, sprinting, mut physics_state) in query.iter_mut() { - let was_sprinting = physics_state.was_sprinting; - if **sprinting != was_sprinting { - let sprinting_action = if **sprinting { - azalea_protocol::packets::game::s_player_command::Action::StartSprinting - } else { - azalea_protocol::packets::game::s_player_command::Action::StopSprinting - }; - send_packet_events.send(SendPacketEvent::new( - entity, - ServerboundPlayerCommand { - id: *minecraft_entity_id, - action: sprinting_action, - data: 0, - }, - )); - physics_state.was_sprinting = **sprinting; - } - } -} - -/// Update the impulse from self.move_direction. The multiplier is used for -/// sneaking. -pub(crate) fn tick_controls(mut query: Query<&mut PhysicsState>) { - for mut physics_state in query.iter_mut() { - let multiplier: Option = None; - - let mut forward_impulse: f32 = 0.; - let mut left_impulse: f32 = 0.; - let move_direction = physics_state.move_direction; - match move_direction { - WalkDirection::Forward | WalkDirection::ForwardRight | WalkDirection::ForwardLeft => { - forward_impulse += 1.; - } - WalkDirection::Backward - | WalkDirection::BackwardRight - | WalkDirection::BackwardLeft => { - forward_impulse -= 1.; - } - _ => {} - }; - match move_direction { - WalkDirection::Right | WalkDirection::ForwardRight | WalkDirection::BackwardRight => { - left_impulse += 1.; - } - WalkDirection::Left | WalkDirection::ForwardLeft | WalkDirection::BackwardLeft => { - left_impulse -= 1.; - } - _ => {} - }; - physics_state.forward_impulse = forward_impulse; - physics_state.left_impulse = left_impulse; - - if let Some(multiplier) = multiplier { - physics_state.forward_impulse *= multiplier; - physics_state.left_impulse *= multiplier; - } - } -} - -/// Makes the bot do one physics tick. Note that this is already handled -/// automatically by the client. -pub fn local_player_ai_step( - mut query: Query< - (&PhysicsState, &mut Physics, &mut Sprinting, &mut Attributes), - With, - >, -) { - for (physics_state, mut physics, mut sprinting, mut attributes) in query.iter_mut() { - // server ai step - physics.x_acceleration = physics_state.left_impulse; - physics.z_acceleration = physics_state.forward_impulse; - - // TODO: food data and abilities - // let has_enough_food_to_sprint = self.food_data().food_level || - // self.abilities().may_fly; - let has_enough_food_to_sprint = true; - - // TODO: double tapping w to sprint i think - - let trying_to_sprint = physics_state.trying_to_sprint; - - if !**sprinting - && ( - // !self.is_in_water() - // || self.is_underwater() && - has_enough_impulse_to_start_sprinting(physics_state) - && has_enough_food_to_sprint - // && !self.using_item() - // && !self.has_effect(MobEffects.BLINDNESS) - && trying_to_sprint - ) - { - set_sprinting(true, &mut sprinting, &mut attributes); - } - } -} - -impl Client { - /// Start walking in the given direction. To sprint, use - /// [`Client::sprint`]. To stop walking, call walk with - /// `WalkDirection::None`. - /// - /// # Examples - /// - /// Walk for 1 second - /// ```rust,no_run - /// # use azalea_client::{Client, WalkDirection}; - /// # use std::time::Duration; - /// # async fn example(mut bot: Client) { - /// bot.walk(WalkDirection::Forward); - /// tokio::time::sleep(Duration::from_secs(1)).await; - /// bot.walk(WalkDirection::None); - /// # } - /// ``` - pub fn walk(&mut self, direction: WalkDirection) { - let mut ecs = self.ecs.lock(); - ecs.send_event(StartWalkEvent { - entity: self.entity, - direction, - }); - } - - /// Start sprinting in the given direction. To stop moving, call - /// [`Client::walk(WalkDirection::None)`] - /// - /// # Examples - /// - /// Sprint for 1 second - /// ```rust,no_run - /// # use azalea_client::{Client, WalkDirection, SprintDirection}; - /// # use std::time::Duration; - /// # async fn example(mut bot: Client) { - /// bot.sprint(SprintDirection::Forward); - /// tokio::time::sleep(Duration::from_secs(1)).await; - /// bot.walk(WalkDirection::None); - /// # } - /// ``` - pub fn sprint(&mut self, direction: SprintDirection) { - let mut ecs = self.ecs.lock(); - ecs.send_event(StartSprintEvent { - entity: self.entity, - direction, - }); - } -} - -/// An event sent when the client starts walking. This does not get sent for -/// non-local entities. -/// -/// To stop walking or sprinting, send this event with `WalkDirection::None`. -#[derive(Event, Debug)] -pub struct StartWalkEvent { - pub entity: Entity, - pub direction: WalkDirection, -} - -/// The system that makes the player start walking when they receive a -/// [`StartWalkEvent`]. -pub fn handle_walk( - mut events: EventReader, - mut query: Query<(&mut PhysicsState, &mut Sprinting, &mut Attributes)>, -) { - for event in events.read() { - if let Ok((mut physics_state, mut sprinting, mut attributes)) = query.get_mut(event.entity) - { - physics_state.move_direction = event.direction; - physics_state.trying_to_sprint = false; - set_sprinting(false, &mut sprinting, &mut attributes); - } - } -} - -/// An event sent when the client starts sprinting. This does not get sent for -/// non-local entities. -#[derive(Event)] -pub struct StartSprintEvent { - pub entity: Entity, - pub direction: SprintDirection, -} -/// The system that makes the player start sprinting when they receive a -/// [`StartSprintEvent`]. -pub fn handle_sprint( - mut query: Query<&mut PhysicsState>, - mut events: EventReader, -) { - for event in events.read() { - if let Ok(mut physics_state) = query.get_mut(event.entity) { - physics_state.move_direction = WalkDirection::from(event.direction); - physics_state.trying_to_sprint = true; - } - } -} - -/// Change whether we're sprinting by adding an attribute modifier to the -/// player. You should use the [`walk`] and [`sprint`] methods instead. -/// Returns if the operation was successful. -fn set_sprinting( - sprinting: bool, - currently_sprinting: &mut Sprinting, - attributes: &mut Attributes, -) -> bool { - **currently_sprinting = sprinting; - if sprinting { - attributes - .speed - .try_insert(azalea_entity::attributes::sprinting_modifier()) - .is_ok() - } else { - attributes - .speed - .remove(&azalea_entity::attributes::sprinting_modifier().id) - .is_none() - } -} - -// Whether the player is moving fast enough to be able to start sprinting. -fn has_enough_impulse_to_start_sprinting(physics_state: &PhysicsState) -> bool { - // if self.underwater() { - // self.has_forward_impulse() - // } else { - physics_state.forward_impulse > 0.8 - // } -} - -/// An event sent by the server that sets or adds to our velocity. Usually -/// `KnockbackKind::Set` is used for normal knockback and `KnockbackKind::Add` -/// is used for explosions, but some servers (notably Hypixel) use explosions -/// for knockback. -#[derive(Event)] -pub struct KnockbackEvent { - pub entity: Entity, - pub knockback: KnockbackType, -} - -pub enum KnockbackType { - Set(Vec3), - Add(Vec3), -} - -pub fn handle_knockback(mut query: Query<&mut Physics>, mut events: EventReader) { - for event in events.read() { - if let Ok(mut physics) = query.get_mut(event.entity) { - match event.knockback { - KnockbackType::Set(velocity) => { - physics.velocity = velocity; - } - KnockbackType::Add(velocity) => { - physics.velocity += velocity; - } - } - } - } -} - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub enum WalkDirection { - #[default] - None, - Forward, - Backward, - Left, - Right, - ForwardRight, - ForwardLeft, - BackwardRight, - BackwardLeft, -} - -/// The directions that we can sprint in. It's a subset of [`WalkDirection`]. -#[derive(Clone, Copy, Debug)] -pub enum SprintDirection { - Forward, - ForwardRight, - ForwardLeft, -} - -impl From for WalkDirection { - fn from(d: SprintDirection) -> Self { - match d { - SprintDirection::Forward => WalkDirection::Forward, - SprintDirection::ForwardRight => WalkDirection::ForwardRight, - SprintDirection::ForwardLeft => WalkDirection::ForwardLeft, - } - } -} diff --git a/azalea-client/src/packet_handling/configuration.rs b/azalea-client/src/packet_handling/configuration.rs deleted file mode 100644 index bfa6914b..00000000 --- a/azalea-client/src/packet_handling/configuration.rs +++ /dev/null @@ -1,271 +0,0 @@ -use std::io::Cursor; - -use azalea_entity::indexing::EntityIdIndex; -use azalea_protocol::packets::config::s_finish_configuration::ServerboundFinishConfiguration; -use azalea_protocol::packets::config::s_keep_alive::ServerboundKeepAlive; -use azalea_protocol::packets::config::s_select_known_packs::ServerboundSelectKnownPacks; -use azalea_protocol::packets::config::{ - self, ClientboundConfigPacket, ServerboundConfigPacket, ServerboundCookieResponse, - ServerboundResourcePack, -}; -use azalea_protocol::packets::{ConnectionProtocol, Packet}; -use azalea_protocol::read::deserialize_packet; -use bevy_ecs::prelude::*; -use bevy_ecs::system::SystemState; -use tracing::{debug, error, warn}; - -use crate::InstanceHolder; -use crate::client::InConfigState; -use crate::disconnect::DisconnectEvent; -use crate::local_player::Hunger; -use crate::packet_handling::game::KeepAliveEvent; -use crate::raw_connection::RawConnection; - -#[derive(Event, Debug, Clone)] -pub struct ConfigurationEvent { - /// The client entity that received the packet. - pub entity: Entity, - /// The packet that was actually received. - pub packet: ClientboundConfigPacket, -} - -pub fn send_packet_events( - query: Query<(Entity, &RawConnection), With>, - mut packet_events: ResMut>, -) { - // we manually clear and send the events at the beginning of each update - // since otherwise it'd cause issues with events in process_packet_events - // running twice - packet_events.clear(); - for (player_entity, raw_conn) in &query { - let packets_lock = raw_conn.incoming_packet_queue(); - let mut packets = packets_lock.lock(); - if !packets.is_empty() { - for raw_packet in packets.iter() { - let packet = match deserialize_packet::(&mut Cursor::new( - raw_packet, - )) { - Ok(packet) => packet, - Err(err) => { - error!("failed to read packet: {err:?}"); - debug!("packet bytes: {raw_packet:?}"); - continue; - } - }; - packet_events.send(ConfigurationEvent { - entity: player_entity, - packet, - }); - } - // clear the packets right after we read them - packets.clear(); - } - } -} - -pub fn process_packet_events(ecs: &mut World) { - let mut events_owned = Vec::new(); - let mut system_state: SystemState> = SystemState::new(ecs); - let mut events = system_state.get_mut(ecs); - for ConfigurationEvent { - entity: player_entity, - packet, - } in events.read() - { - // we do this so `ecs` isn't borrowed for the whole loop - events_owned.push((*player_entity, packet.clone())); - } - for (player_entity, packet) in events_owned { - match packet { - ClientboundConfigPacket::RegistryData(p) => { - let mut system_state: SystemState> = - SystemState::new(ecs); - let mut query = system_state.get_mut(ecs); - let instance_holder = query.get_mut(player_entity).unwrap(); - let mut instance = instance_holder.instance.write(); - - // add the new registry data - instance.registries.append(p.registry_id, p.entries); - } - - ClientboundConfigPacket::CustomPayload(p) => { - debug!("Got custom payload packet {p:?}"); - } - ClientboundConfigPacket::Disconnect(p) => { - warn!("Got disconnect packet {p:?}"); - let mut system_state: SystemState> = - SystemState::new(ecs); - let mut disconnect_events = system_state.get_mut(ecs); - disconnect_events.send(DisconnectEvent { - entity: player_entity, - reason: Some(p.reason.clone()), - }); - } - ClientboundConfigPacket::FinishConfiguration(p) => { - debug!("got FinishConfiguration packet: {p:?}"); - - let mut system_state: SystemState> = - SystemState::new(ecs); - let mut query = system_state.get_mut(ecs); - let mut raw_conn = query.get_mut(player_entity).unwrap(); - - raw_conn - .write_packet(ServerboundFinishConfiguration) - .expect( - "we should be in the right state and encoding this packet shouldn't fail", - ); - raw_conn.set_state(ConnectionProtocol::Game); - - // these components are added now that we're going to be in the Game state - ecs.entity_mut(player_entity) - .remove::() - .insert(crate::JoinedClientBundle { - physics_state: crate::PhysicsState::default(), - inventory: crate::inventory::Inventory::default(), - tab_list: crate::local_player::TabList::default(), - current_sequence_number: crate::interact::CurrentSequenceNumber::default(), - last_sent_direction: crate::movement::LastSentLookDirection::default(), - abilities: crate::local_player::PlayerAbilities::default(), - permission_level: crate::local_player::PermissionLevel::default(), - hunger: Hunger::default(), - chunk_batch_info: crate::chunks::ChunkBatchInfo::default(), - - entity_id_index: EntityIdIndex::default(), - - mining: crate::mining::MineBundle::default(), - attack: crate::attack::AttackBundle::default(), - - _local_entity: azalea_entity::LocalEntity, - }); - } - ClientboundConfigPacket::KeepAlive(p) => { - debug!("Got keep alive packet (in configuration) {p:?} for {player_entity:?}"); - - let mut system_state: SystemState<( - Query<&RawConnection>, - EventWriter, - )> = SystemState::new(ecs); - let (query, mut keepalive_events) = system_state.get_mut(ecs); - let raw_conn = query.get(player_entity).unwrap(); - - keepalive_events.send(KeepAliveEvent { - entity: player_entity, - id: p.id, - }); - raw_conn - .write_packet(ServerboundKeepAlive { id: p.id }) - .unwrap(); - } - ClientboundConfigPacket::Ping(p) => { - debug!("Got ping packet {p:?}"); - - let mut system_state: SystemState> = SystemState::new(ecs); - let mut query = system_state.get_mut(ecs); - let raw_conn = query.get_mut(player_entity).unwrap(); - - raw_conn - .write_packet(config::s_pong::ServerboundPong { id: p.id }) - .unwrap(); - } - ClientboundConfigPacket::ResourcePackPush(p) => { - debug!("Got resource pack packet {p:?}"); - - let mut system_state: SystemState> = SystemState::new(ecs); - let mut query = system_state.get_mut(ecs); - let raw_conn = query.get_mut(player_entity).unwrap(); - - // always accept resource pack - raw_conn - .write_packet(ServerboundResourcePack { - id: p.id, - action: config::s_resource_pack::Action::Accepted, - }) - .unwrap(); - } - ClientboundConfigPacket::ResourcePackPop(_) => { - // we can ignore this - } - ClientboundConfigPacket::UpdateEnabledFeatures(p) => { - debug!("Got update enabled features packet {p:?}"); - } - ClientboundConfigPacket::UpdateTags(_p) => { - debug!("Got update tags packet"); - } - ClientboundConfigPacket::CookieRequest(p) => { - debug!("Got cookie request packet {p:?}"); - - let mut system_state: SystemState> = SystemState::new(ecs); - let mut query = system_state.get_mut(ecs); - let raw_conn = query.get_mut(player_entity).unwrap(); - - raw_conn - .write_packet(ServerboundCookieResponse { - key: p.key, - // cookies aren't implemented - payload: None, - }) - .unwrap(); - } - ClientboundConfigPacket::ResetChat(p) => { - debug!("Got reset chat packet {p:?}"); - } - ClientboundConfigPacket::StoreCookie(p) => { - debug!("Got store cookie packet {p:?}"); - } - ClientboundConfigPacket::Transfer(p) => { - debug!("Got transfer packet {p:?}"); - } - ClientboundConfigPacket::SelectKnownPacks(p) => { - debug!("Got select known packs packet {p:?}"); - - let mut system_state: SystemState> = SystemState::new(ecs); - let mut query = system_state.get_mut(ecs); - let raw_conn = query.get_mut(player_entity).unwrap(); - - // resource pack management isn't implemented - raw_conn - .write_packet(ServerboundSelectKnownPacks { - known_packs: vec![], - }) - .unwrap(); - } - ClientboundConfigPacket::ServerLinks(_) => {} - ClientboundConfigPacket::CustomReportDetails(_) => {} - } - } -} - -/// An event for sending a packet to the server while we're in the -/// `configuration` state. -#[derive(Event)] -pub struct SendConfigurationEvent { - pub sent_by: Entity, - pub packet: ServerboundConfigPacket, -} -impl SendConfigurationEvent { - pub fn new(sent_by: Entity, packet: impl Packet) -> Self { - let packet = packet.into_variant(); - Self { sent_by, packet } - } -} - -pub fn handle_send_packet_event( - mut send_packet_events: EventReader, - mut query: Query<(&mut RawConnection, Option<&InConfigState>)>, -) { - for event in send_packet_events.read() { - if let Ok((raw_conn, in_configuration_state)) = query.get_mut(event.sent_by) { - if in_configuration_state.is_none() { - error!( - "Tried to send a configuration packet {:?} while not in configuration state", - event.packet - ); - continue; - } - debug!("Sending packet: {:?}", event.packet); - if let Err(e) = raw_conn.write_packet(event.packet.clone()) { - error!("Failed to send packet: {e}"); - } - } - } -} diff --git a/azalea-client/src/packet_handling/game.rs b/azalea-client/src/packet_handling/game.rs deleted file mode 100644 index 6f2868e9..00000000 --- a/azalea-client/src/packet_handling/game.rs +++ /dev/null @@ -1,1585 +0,0 @@ -use std::{ - collections::HashSet, - io::Cursor, - ops::Add, - sync::{Arc, Weak}, -}; - -use azalea_chat::FormattedText; -use azalea_core::{ - game_type::GameMode, - math, - position::{ChunkPos, Vec3}, - resource_location::ResourceLocation, -}; -use azalea_entity::{ - Dead, EntityBundle, EntityKind, LastSentPosition, LoadedBy, LocalEntity, LookDirection, - Physics, Position, RelativeEntityUpdate, - indexing::{EntityIdIndex, EntityUuidIndex}, - metadata::{Health, apply_metadata}, -}; -use azalea_protocol::{ - packets::{ - Packet, - game::{ - ClientboundGamePacket, ServerboundGamePacket, - c_player_combat_kill::ClientboundPlayerCombatKill, - s_accept_teleportation::ServerboundAcceptTeleportation, - s_configuration_acknowledged::ServerboundConfigurationAcknowledged, - s_keep_alive::ServerboundKeepAlive, s_move_player_pos_rot::ServerboundMovePlayerPosRot, - s_pong::ServerboundPong, - }, - }, - read::deserialize_packet, -}; -use azalea_world::{Instance, InstanceContainer, InstanceName, MinecraftEntityId, PartialInstance}; -use bevy_ecs::{prelude::*, system::SystemState}; -use parking_lot::RwLock; -use tracing::{debug, error, trace, warn}; -use uuid::Uuid; - -use crate::{ - ClientInformation, PlayerInfo, - chat::{ChatPacket, ChatReceivedEvent}, - chunks, - disconnect::DisconnectEvent, - inventory::{ - ClientSideCloseContainerEvent, Inventory, MenuOpenedEvent, SetContainerContentEvent, - }, - local_player::{ - GameProfileComponent, Hunger, InstanceHolder, LocalGameMode, PlayerAbilities, TabList, - }, - movement::{KnockbackEvent, KnockbackType}, - raw_connection::RawConnection, -}; - -/// An event that's sent when we receive a packet. -/// ``` -/// # use azalea_client::packet_handling::game::PacketEvent; -/// # use azalea_protocol::packets::game::ClientboundGamePacket; -/// # use bevy_ecs::event::EventReader; -/// -/// fn handle_packets(mut events: EventReader) { -/// for PacketEvent { -/// entity, -/// packet, -/// } in events.read() { -/// match packet.as_ref() { -/// ClientboundGamePacket::LevelParticles(p) => { -/// // ... -/// } -/// _ => {} -/// } -/// } -/// } -/// ``` -#[derive(Event, Debug, Clone)] -pub struct PacketEvent { - /// The client entity that received the packet. - pub entity: Entity, - /// The packet that was actually received. - pub packet: Arc, -} - -/// A player joined the game (or more specifically, was added to the tab -/// list of a local player). -#[derive(Event, Debug, Clone)] -pub struct AddPlayerEvent { - /// The local player entity that received this event. - pub entity: Entity, - pub info: PlayerInfo, -} -/// A player left the game (or maybe is still in the game and was just -/// removed from the tab list of a local player). -#[derive(Event, Debug, Clone)] -pub struct RemovePlayerEvent { - /// The local player entity that received this event. - pub entity: Entity, - pub info: PlayerInfo, -} -/// A player was updated in the tab list of a local player (gamemode, display -/// name, or latency changed). -#[derive(Event, Debug, Clone)] -pub struct UpdatePlayerEvent { - /// The local player entity that received this event. - pub entity: Entity, - pub info: PlayerInfo, -} - -/// Event for when an entity dies. dies. If it's a local player and there's a -/// reason in the death screen, the [`ClientboundPlayerCombatKill`] will -/// be included. -#[derive(Event, Debug, Clone)] -pub struct DeathEvent { - pub entity: Entity, - pub packet: Option, -} - -/// A KeepAlive packet is sent from the server to verify that the client is -/// still connected. -#[derive(Event, Debug, Clone)] -pub struct KeepAliveEvent { - pub entity: Entity, - /// The ID of the keepalive. This is an arbitrary number, but vanilla - /// servers use the time to generate this. - pub id: u64, -} - -#[derive(Event, Debug, Clone)] -pub struct ResourcePackEvent { - pub entity: Entity, - /// The random ID for this request to download the resource pack. The packet - /// for replying to a resource pack push must contain the same ID. - pub id: Uuid, - pub url: String, - pub hash: String, - pub required: bool, - pub prompt: Option, -} - -/// An instance (aka world, dimension) was loaded by a client. -/// -/// Since the instance is given to you as a weak reference, it won't be able to -/// be `upgrade`d if all local players leave it. -#[derive(Event, Debug, Clone)] -pub struct InstanceLoadedEvent { - pub entity: Entity, - pub name: ResourceLocation, - pub instance: Weak>, -} - -pub fn send_packet_events( - query: Query<(Entity, &RawConnection), With>, - mut packet_events: ResMut>, -) { - // we manually clear and send the events at the beginning of each update - // since otherwise it'd cause issues with events in process_packet_events - // running twice - packet_events.clear(); - for (player_entity, raw_connection) in &query { - let packets_lock = raw_connection.incoming_packet_queue(); - let mut packets = packets_lock.lock(); - if !packets.is_empty() { - for raw_packet in packets.iter() { - let packet = - match deserialize_packet::(&mut Cursor::new(raw_packet)) - { - Ok(packet) => packet, - Err(err) => { - error!("failed to read packet: {err:?}"); - debug!("packet bytes: {raw_packet:?}"); - continue; - } - }; - packet_events.send(PacketEvent { - entity: player_entity, - packet: Arc::new(packet), - }); - } - // clear the packets right after we read them - packets.clear(); - } - } -} - -pub fn process_packet_events(ecs: &mut World) { - let mut events_owned = Vec::<(Entity, Arc)>::new(); - { - let mut system_state = SystemState::>::new(ecs); - let mut events = system_state.get_mut(ecs); - for PacketEvent { - entity: player_entity, - packet, - } in events.read() - { - // we do this so `ecs` isn't borrowed for the whole loop - events_owned.push((*player_entity, packet.clone())); - } - } - for (player_entity, packet) in events_owned { - let packet_clone = packet.clone(); - let packet_ref = packet_clone.as_ref(); - match packet_ref { - ClientboundGamePacket::Login(p) => { - debug!("Got login packet"); - - #[allow(clippy::type_complexity)] - let mut system_state: SystemState<( - Commands, - Query<( - &GameProfileComponent, - &ClientInformation, - Option<&mut InstanceName>, - Option<&mut LoadedBy>, - &mut EntityIdIndex, - &mut InstanceHolder, - )>, - EventWriter, - ResMut, - ResMut, - EventWriter, - )> = SystemState::new(ecs); - let ( - mut commands, - mut query, - mut instance_loaded_events, - mut instance_container, - mut entity_uuid_index, - mut send_packet_events, - ) = system_state.get_mut(ecs); - let ( - game_profile, - client_information, - instance_name, - loaded_by, - mut entity_id_index, - mut instance_holder, - ) = query.get_mut(player_entity).unwrap(); - - { - let new_instance_name = p.common.dimension.clone(); - - if let Some(mut instance_name) = instance_name { - *instance_name = instance_name.clone(); - } else { - commands - .entity(player_entity) - .insert(InstanceName(new_instance_name.clone())); - } - - let Some((_dimension_type, dimension_data)) = p - .common - .dimension_type(&instance_holder.instance.read().registries) - else { - continue; - }; - - // add this world to the instance_container (or don't if it's already - // there) - let weak_instance = instance_container.insert( - new_instance_name.clone(), - dimension_data.height, - dimension_data.min_y, - &instance_holder.instance.read().registries, - ); - instance_loaded_events.send(InstanceLoadedEvent { - entity: player_entity, - name: new_instance_name.clone(), - instance: Arc::downgrade(&weak_instance), - }); - - // set the partial_world to an empty world - // (when we add chunks or entities those will be in the - // instance_container) - - *instance_holder.partial_instance.write() = PartialInstance::new( - azalea_world::chunk_storage::calculate_chunk_storage_range( - client_information.view_distance.into(), - ), - // this argument makes it so other clients don't update this player entity - // in a shared instance - Some(player_entity), - ); - { - let map = instance_holder.instance.read().registries.map.clone(); - let new_registries = &mut weak_instance.write().registries; - // add the registries from this instance to the weak instance - for (registry_name, registry) in map { - new_registries.map.insert(registry_name, registry); - } - } - instance_holder.instance = weak_instance; - - let entity_bundle = EntityBundle::new( - game_profile.uuid, - Vec3::default(), - azalea_registry::EntityKind::Player, - new_instance_name, - ); - let entity_id = p.player_id; - // insert our components into the ecs :) - commands.entity(player_entity).insert(( - entity_id, - LocalGameMode { - current: p.common.game_type, - previous: p.common.previous_game_type.into(), - }, - entity_bundle, - )); - - azalea_entity::indexing::add_entity_to_indexes( - entity_id, - player_entity, - Some(game_profile.uuid), - &mut entity_id_index, - &mut entity_uuid_index, - &mut instance_holder.instance.write(), - ); - - // update or insert loaded_by - if let Some(mut loaded_by) = loaded_by { - loaded_by.insert(player_entity); - } else { - commands - .entity(player_entity) - .insert(LoadedBy(HashSet::from_iter(vec![player_entity]))); - } - } - - // send the client information that we have set - debug!( - "Sending client information because login: {:?}", - client_information - ); - send_packet_events.send(SendPacketEvent::new(player_entity, - azalea_protocol::packets::game::s_client_information::ServerboundClientInformation { information: client_information.clone() }, - )); - - system_state.apply(ecs); - } - ClientboundGamePacket::SetChunkCacheRadius(p) => { - debug!("Got set chunk cache radius packet {p:?}"); - } - - ClientboundGamePacket::ChunkBatchStart(_p) => { - // the packet is empty, just a marker to tell us when the batch starts and ends - debug!("Got chunk batch start"); - let mut system_state: SystemState> = - SystemState::new(ecs); - let mut chunk_batch_start_events = system_state.get_mut(ecs); - - chunk_batch_start_events.send(chunks::ChunkBatchStartEvent { - entity: player_entity, - }); - } - ClientboundGamePacket::ChunkBatchFinished(p) => { - debug!("Got chunk batch finished {p:?}"); - - let mut system_state: SystemState> = - SystemState::new(ecs); - let mut chunk_batch_start_events = system_state.get_mut(ecs); - - chunk_batch_start_events.send(chunks::ChunkBatchFinishedEvent { - entity: player_entity, - batch_size: p.batch_size, - }); - } - - ClientboundGamePacket::CustomPayload(p) => { - debug!("Got custom payload packet {p:?}"); - } - ClientboundGamePacket::ChangeDifficulty(p) => { - debug!("Got difficulty packet {p:?}"); - } - ClientboundGamePacket::Commands(_p) => { - debug!("Got declare commands packet"); - } - ClientboundGamePacket::PlayerAbilities(p) => { - debug!("Got player abilities packet {p:?}"); - let mut system_state: SystemState> = - SystemState::new(ecs); - let mut query = system_state.get_mut(ecs); - let mut player_abilities = query.get_mut(player_entity).unwrap(); - - *player_abilities = PlayerAbilities::from(p); - } - ClientboundGamePacket::SetCursorItem(p) => { - debug!("Got set cursor item packet {p:?}"); - } - ClientboundGamePacket::UpdateTags(_p) => { - debug!("Got update tags packet"); - } - ClientboundGamePacket::Disconnect(p) => { - warn!("Got disconnect packet {p:?}"); - let mut system_state: SystemState> = - SystemState::new(ecs); - let mut disconnect_events = system_state.get_mut(ecs); - disconnect_events.send(DisconnectEvent { - entity: player_entity, - reason: Some(p.reason.clone()), - }); - } - ClientboundGamePacket::UpdateRecipes(_p) => { - debug!("Got update recipes packet"); - } - ClientboundGamePacket::EntityEvent(_p) => { - // debug!("Got entity event packet {p:?}"); - } - ClientboundGamePacket::PlayerPosition(p) => { - debug!("Got player position packet {p:?}"); - - #[allow(clippy::type_complexity)] - let mut system_state: SystemState<( - Query<( - &mut Physics, - &mut LookDirection, - &mut Position, - &mut LastSentPosition, - )>, - EventWriter, - )> = SystemState::new(ecs); - let (mut query, mut send_packet_events) = system_state.get_mut(ecs); - let Ok((mut physics, mut direction, mut position, mut last_sent_position)) = - query.get_mut(player_entity) - else { - continue; - }; - - **last_sent_position = **position; - - fn apply_change>(base: T, condition: bool, change: T) -> T { - if condition { base + change } else { change } - } - - let new_x = apply_change(position.x, p.relative.x, p.change.pos.x); - let new_y = apply_change(position.y, p.relative.y, p.change.pos.y); - let new_z = apply_change(position.z, p.relative.z, p.change.pos.z); - - let new_y_rot = apply_change( - direction.y_rot, - p.relative.y_rot, - p.change.look_direction.y_rot, - ); - let new_x_rot = apply_change( - direction.x_rot, - p.relative.x_rot, - p.change.look_direction.x_rot, - ); - - let mut new_delta_from_rotations = physics.velocity; - if p.relative.rotate_delta { - let y_rot_delta = direction.y_rot - new_y_rot; - let x_rot_delta = direction.x_rot - new_x_rot; - new_delta_from_rotations = new_delta_from_rotations - .x_rot(math::to_radians(x_rot_delta as f64) as f32) - .y_rot(math::to_radians(y_rot_delta as f64) as f32); - } - - let new_delta = Vec3::new( - apply_change( - new_delta_from_rotations.x, - p.relative.delta_x, - p.change.delta.x, - ), - apply_change( - new_delta_from_rotations.y, - p.relative.delta_y, - p.change.delta.y, - ), - apply_change( - new_delta_from_rotations.z, - p.relative.delta_z, - p.change.delta.z, - ), - ); - - // apply the updates - - physics.velocity = new_delta; - - (direction.y_rot, direction.x_rot) = (new_y_rot, new_x_rot); - - let new_pos = Vec3::new(new_x, new_y, new_z); - if new_pos != **position { - **position = new_pos; - } - - // old_pos is set to the current position when we're teleported - physics.set_old_pos(&position); - - // send the relevant packets - - send_packet_events.send(SendPacketEvent::new( - player_entity, - ServerboundAcceptTeleportation { id: p.id }, - )); - send_packet_events.send(SendPacketEvent::new( - player_entity, - ServerboundMovePlayerPosRot { - pos: new_pos, - look_direction: LookDirection::new(new_y_rot, new_x_rot), - // this is always false - on_ground: false, - }, - )); - } - ClientboundGamePacket::PlayerInfoUpdate(p) => { - debug!("Got player info packet {p:?}"); - - #[allow(clippy::type_complexity)] - let mut system_state: SystemState<( - Query<&mut TabList>, - EventWriter, - EventWriter, - ResMut, - )> = SystemState::new(ecs); - let ( - mut query, - mut add_player_events, - mut update_player_events, - mut tab_list_resource, - ) = system_state.get_mut(ecs); - let mut tab_list = query.get_mut(player_entity).unwrap(); - - for updated_info in &p.entries { - // add the new player maybe - if p.actions.add_player { - let info = PlayerInfo { - profile: updated_info.profile.clone(), - uuid: updated_info.profile.uuid, - gamemode: updated_info.game_mode, - latency: updated_info.latency, - display_name: updated_info.display_name.clone(), - }; - tab_list.insert(updated_info.profile.uuid, info.clone()); - add_player_events.send(AddPlayerEvent { - entity: player_entity, - info: info.clone(), - }); - } else if let Some(info) = tab_list.get_mut(&updated_info.profile.uuid) { - // `else if` because the block for add_player above - // already sets all the fields - if p.actions.update_game_mode { - info.gamemode = updated_info.game_mode; - } - if p.actions.update_latency { - info.latency = updated_info.latency; - } - if p.actions.update_display_name { - info.display_name.clone_from(&updated_info.display_name); - } - update_player_events.send(UpdatePlayerEvent { - entity: player_entity, - info: info.clone(), - }); - } else { - let uuid = updated_info.profile.uuid; - #[cfg(debug_assertions)] - warn!("Ignoring PlayerInfoUpdate for unknown player {uuid}"); - #[cfg(not(debug_assertions))] - debug!("Ignoring PlayerInfoUpdate for unknown player {uuid}"); - } - } - - *tab_list_resource = tab_list.clone(); - } - ClientboundGamePacket::PlayerInfoRemove(p) => { - let mut system_state: SystemState<( - Query<&mut TabList>, - EventWriter, - ResMut, - )> = SystemState::new(ecs); - let (mut query, mut remove_player_events, mut tab_list_resource) = - system_state.get_mut(ecs); - let mut tab_list = query.get_mut(player_entity).unwrap(); - - for uuid in &p.profile_ids { - if let Some(info) = tab_list.remove(uuid) { - remove_player_events.send(RemovePlayerEvent { - entity: player_entity, - info, - }); - } - tab_list_resource.remove(uuid); - } - } - ClientboundGamePacket::SetChunkCacheCenter(p) => { - debug!("Got chunk cache center packet {p:?}"); - - let mut system_state: SystemState> = - SystemState::new(ecs); - let mut query = system_state.get_mut(ecs); - let instance_holder = query.get_mut(player_entity).unwrap(); - let mut partial_world = instance_holder.partial_instance.write(); - - partial_world - .chunks - .update_view_center(ChunkPos::new(p.x, p.z)); - } - ClientboundGamePacket::ChunksBiomes(_) => {} - ClientboundGamePacket::LightUpdate(_p) => { - // debug!("Got light update packet {p:?}"); - } - ClientboundGamePacket::LevelChunkWithLight(p) => { - debug!("Got chunk with light packet {} {}", p.x, p.z); - - let mut system_state: SystemState> = - SystemState::new(ecs); - let mut receive_chunk_events = system_state.get_mut(ecs); - receive_chunk_events.send(chunks::ReceiveChunkEvent { - entity: player_entity, - packet: p.clone(), - }); - } - ClientboundGamePacket::AddEntity(p) => { - debug!("Got add entity packet {p:?}"); - - #[allow(clippy::type_complexity)] - let mut system_state: SystemState<( - Commands, - Query<(&mut EntityIdIndex, Option<&InstanceName>, Option<&TabList>)>, - Query<&mut LoadedBy>, - Query, - Res, - ResMut, - )> = SystemState::new(ecs); - let ( - mut commands, - mut query, - mut loaded_by_query, - entity_query, - instance_container, - mut entity_uuid_index, - ) = system_state.get_mut(ecs); - let (mut entity_id_index, instance_name, tab_list) = - query.get_mut(player_entity).unwrap(); - - let entity_id = p.id; - - let Some(instance_name) = instance_name else { - warn!("got add player packet but we haven't gotten a login packet yet"); - continue; - }; - - // check if the entity already exists, and if it does then only add to LoadedBy - let instance = instance_container.get(instance_name).unwrap(); - if let Some(&ecs_entity) = instance.read().entity_by_id.get(&entity_id) { - // entity already exists - let Ok(mut loaded_by) = loaded_by_query.get_mut(ecs_entity) else { - // LoadedBy for this entity isn't in the ecs! figure out what went wrong - // and print an error - - let entity_in_ecs = entity_query.get(ecs_entity).is_ok(); - - if entity_in_ecs { - error!( - "LoadedBy for entity {entity_id:?} ({ecs_entity:?}) isn't in the ecs, but the entity is in entity_by_id" - ); - } else { - error!( - "Entity {entity_id:?} ({ecs_entity:?}) isn't in the ecs, but the entity is in entity_by_id" - ); - } - continue; - }; - loaded_by.insert(player_entity); - - // per-client id index - entity_id_index.insert(entity_id, ecs_entity); - - debug!("added to LoadedBy of entity {ecs_entity:?} with id {entity_id:?}"); - continue; - }; - - // entity doesn't exist in the global index! - - let bundle = p.as_entity_bundle((**instance_name).clone()); - let mut spawned = - commands.spawn((entity_id, LoadedBy(HashSet::from([player_entity])), bundle)); - let ecs_entity: Entity = spawned.id(); - debug!("spawned entity {ecs_entity:?} with id {entity_id:?}"); - - azalea_entity::indexing::add_entity_to_indexes( - entity_id, - ecs_entity, - Some(p.uuid), - &mut entity_id_index, - &mut entity_uuid_index, - &mut instance.write(), - ); - - // add the GameProfileComponent if the uuid is in the tab list - if let Some(tab_list) = tab_list { - // (technically this makes it possible for non-player entities to have - // GameProfileComponents but the server would have to be doing something - // really weird) - if let Some(player_info) = tab_list.get(&p.uuid) { - spawned.insert(GameProfileComponent(player_info.profile.clone())); - } - } - - // the bundle doesn't include the default entity metadata so we add that - // separately - p.apply_metadata(&mut spawned); - - system_state.apply(ecs); - } - ClientboundGamePacket::SetEntityData(p) => { - debug!("Got set entity data packet {p:?}"); - - #[allow(clippy::type_complexity)] - let mut system_state: SystemState<( - Commands, - Query<(&EntityIdIndex, &InstanceHolder)>, - Query<&EntityKind>, - )> = SystemState::new(ecs); - let (mut commands, mut query, entity_kind_query) = system_state.get_mut(ecs); - let (entity_id_index, instance_holder) = query.get_mut(player_entity).unwrap(); - - let entity = entity_id_index.get(p.id); - - let Some(entity) = entity else { - // some servers like hypixel trigger this a lot :( - debug!( - "Server sent an entity data packet for an entity id ({}) that we don't know about", - p.id - ); - continue; - }; - let entity_kind = *entity_kind_query.get(entity).unwrap(); - - let packed_items = p.packed_items.clone().to_vec(); - - // we use RelativeEntityUpdate because it makes sure changes aren't made - // multiple times - commands.entity(entity).queue(RelativeEntityUpdate { - partial_world: instance_holder.partial_instance.clone(), - update: Box::new(move |entity| { - let entity_id = entity.id(); - entity.world_scope(|world| { - let mut commands_system_state = SystemState::::new(world); - let mut commands = commands_system_state.get_mut(world); - let mut entity_commands = commands.entity(entity_id); - if let Err(e) = - apply_metadata(&mut entity_commands, *entity_kind, packed_items) - { - warn!("{e}"); - } - commands_system_state.apply(world); - }); - }), - }); - - system_state.apply(ecs); - } - ClientboundGamePacket::UpdateAttributes(_p) => { - // debug!("Got update attributes packet {p:?}"); - } - ClientboundGamePacket::SetEntityMotion(p) => { - // vanilla servers use this packet for knockback, but note that the Explode - // packet is also sometimes used by servers for knockback - - let mut system_state: SystemState<( - Commands, - Query<(&EntityIdIndex, &InstanceHolder)>, - )> = SystemState::new(ecs); - let (mut commands, mut query) = system_state.get_mut(ecs); - let (entity_id_index, instance_holder) = query.get_mut(player_entity).unwrap(); - - let Some(entity) = entity_id_index.get(p.id) else { - // note that this log (and some other ones like the one in RemoveEntities) - // sometimes happens when killing mobs. it seems to be a vanilla bug, which is - // why it's a debug log instead of a warning - debug!( - "Got set entity motion packet for unknown entity id {}", - p.id - ); - continue; - }; - - // this is to make sure the same entity velocity update doesn't get sent - // multiple times when in swarms - - let knockback = KnockbackType::Set(Vec3 { - x: p.xa as f64 / 8000., - y: p.ya as f64 / 8000., - z: p.za as f64 / 8000., - }); - - commands.entity(entity).queue(RelativeEntityUpdate { - partial_world: instance_holder.partial_instance.clone(), - update: Box::new(move |entity_mut| { - entity_mut.world_scope(|world| { - world.send_event(KnockbackEvent { entity, knockback }) - }); - }), - }); - - system_state.apply(ecs); - } - ClientboundGamePacket::SetEntityLink(p) => { - debug!("Got set entity link packet {p:?}"); - } - ClientboundGamePacket::InitializeBorder(p) => { - debug!("Got initialize border packet {p:?}"); - } - ClientboundGamePacket::SetTime(_p) => { - // debug!("Got set time packet {p:?}"); - } - ClientboundGamePacket::SetDefaultSpawnPosition(p) => { - debug!("Got set default spawn position packet {p:?}"); - } - ClientboundGamePacket::SetHealth(p) => { - debug!("Got set health packet {p:?}"); - - let mut system_state: SystemState> = - SystemState::new(ecs); - let mut query = system_state.get_mut(ecs); - let (mut health, mut hunger) = query.get_mut(player_entity).unwrap(); - - **health = p.health; - (hunger.food, hunger.saturation) = (p.food, p.saturation); - - // the `Dead` component is added by the `update_dead` system - // in azalea-world and then the `dead_event` system fires - // the Death event. - } - ClientboundGamePacket::SetExperience(p) => { - debug!("Got set experience packet {p:?}"); - } - ClientboundGamePacket::TeleportEntity(p) => { - let mut system_state: SystemState<( - Commands, - Query<(&EntityIdIndex, &InstanceHolder)>, - )> = SystemState::new(ecs); - let (mut commands, mut query) = system_state.get_mut(ecs); - let (entity_id_index, instance_holder) = query.get_mut(player_entity).unwrap(); - - let Some(entity) = entity_id_index.get(p.id) else { - warn!("Got teleport entity packet for unknown entity id {}", p.id); - continue; - }; - - let new_pos = p.change.pos; - let new_look_direction = LookDirection { - x_rot: (p.change.look_direction.x_rot as i32 * 360) as f32 / 256., - y_rot: (p.change.look_direction.y_rot as i32 * 360) as f32 / 256., - }; - commands.entity(entity).queue(RelativeEntityUpdate { - partial_world: instance_holder.partial_instance.clone(), - update: Box::new(move |entity| { - let mut position = entity.get_mut::().unwrap(); - if new_pos != **position { - **position = new_pos; - } - let position = *position; - let mut look_direction = entity.get_mut::().unwrap(); - if new_look_direction != *look_direction { - *look_direction = new_look_direction; - } - // old_pos is set to the current position when we're teleported - let mut physics = entity.get_mut::().unwrap(); - physics.set_old_pos(&position); - }), - }); - - system_state.apply(ecs); - } - ClientboundGamePacket::UpdateAdvancements(p) => { - debug!("Got update advancements packet {p:?}"); - } - ClientboundGamePacket::RotateHead(_p) => { - // debug!("Got rotate head packet {p:?}"); - } - ClientboundGamePacket::MoveEntityPos(p) => { - let mut system_state: SystemState<( - Commands, - Query<(&EntityIdIndex, &InstanceHolder)>, - )> = SystemState::new(ecs); - let (mut commands, mut query) = system_state.get_mut(ecs); - let (entity_id_index, instance_holder) = query.get_mut(player_entity).unwrap(); - - debug!("Got move entity pos packet {p:?}"); - - let Some(entity) = entity_id_index.get(p.entity_id) else { - debug!( - "Got move entity pos packet for unknown entity id {}", - p.entity_id - ); - continue; - }; - - let new_delta = p.delta.clone(); - let new_on_ground = p.on_ground; - commands.entity(entity).queue(RelativeEntityUpdate { - partial_world: instance_holder.partial_instance.clone(), - update: Box::new(move |entity_mut| { - let mut physics = entity_mut.get_mut::().unwrap(); - let new_pos = physics.vec_delta_codec.decode( - new_delta.xa as i64, - new_delta.ya as i64, - new_delta.za as i64, - ); - physics.vec_delta_codec.set_base(new_pos); - physics.set_on_ground(new_on_ground); - - let mut position = entity_mut.get_mut::().unwrap(); - if new_pos != **position { - **position = new_pos; - } - }), - }); - - system_state.apply(ecs); - } - ClientboundGamePacket::MoveEntityPosRot(p) => { - let mut system_state: SystemState<( - Commands, - Query<(&EntityIdIndex, &InstanceHolder)>, - )> = SystemState::new(ecs); - let (mut commands, mut query) = system_state.get_mut(ecs); - let (entity_id_index, instance_holder) = query.get_mut(player_entity).unwrap(); - - debug!("Got move entity pos rot packet {p:?}"); - - let entity = entity_id_index.get(p.entity_id); - - if let Some(entity) = entity { - let new_delta = p.delta.clone(); - let new_look_direction = LookDirection { - x_rot: (p.x_rot as i32 * 360) as f32 / 256., - y_rot: (p.y_rot as i32 * 360) as f32 / 256., - }; - - let new_on_ground = p.on_ground; - - commands.entity(entity).queue(RelativeEntityUpdate { - partial_world: instance_holder.partial_instance.clone(), - update: Box::new(move |entity_mut| { - let mut physics = entity_mut.get_mut::().unwrap(); - let new_pos = physics.vec_delta_codec.decode( - new_delta.xa as i64, - new_delta.ya as i64, - new_delta.za as i64, - ); - physics.vec_delta_codec.set_base(new_pos); - physics.set_on_ground(new_on_ground); - - let mut position = entity_mut.get_mut::().unwrap(); - if new_pos != **position { - **position = new_pos; - } - - let mut look_direction = entity_mut.get_mut::().unwrap(); - if new_look_direction != *look_direction { - *look_direction = new_look_direction; - } - }), - }); - } else { - // often triggered by hypixel :( - debug!( - "Got move entity pos rot packet for unknown entity id {}", - p.entity_id - ); - } - - system_state.apply(ecs); - } - - ClientboundGamePacket::MoveEntityRot(p) => { - let mut system_state: SystemState<( - Commands, - Query<(&EntityIdIndex, &InstanceHolder)>, - )> = SystemState::new(ecs); - let (mut commands, mut query) = system_state.get_mut(ecs); - let (entity_id_index, instance_holder) = query.get_mut(player_entity).unwrap(); - - let entity = entity_id_index.get(p.entity_id); - - if let Some(entity) = entity { - let new_look_direction = LookDirection { - x_rot: (p.x_rot as i32 * 360) as f32 / 256., - y_rot: (p.y_rot as i32 * 360) as f32 / 256., - }; - let new_on_ground = p.on_ground; - - commands.entity(entity).queue(RelativeEntityUpdate { - partial_world: instance_holder.partial_instance.clone(), - update: Box::new(move |entity_mut| { - let mut physics = entity_mut.get_mut::().unwrap(); - physics.set_on_ground(new_on_ground); - - let mut look_direction = entity_mut.get_mut::().unwrap(); - if new_look_direction != *look_direction { - *look_direction = new_look_direction; - } - }), - }); - } else { - warn!( - "Got move entity rot packet for unknown entity id {}", - p.entity_id - ); - } - - system_state.apply(ecs); - } - ClientboundGamePacket::KeepAlive(p) => { - debug!("Got keep alive packet {p:?} for {player_entity:?}"); - - let mut system_state: SystemState<( - EventWriter, - EventWriter, - )> = SystemState::new(ecs); - let (mut keepalive_events, mut send_packet_events) = system_state.get_mut(ecs); - - keepalive_events.send(KeepAliveEvent { - entity: player_entity, - id: p.id, - }); - send_packet_events.send(SendPacketEvent::new( - player_entity, - ServerboundKeepAlive { id: p.id }, - )); - } - ClientboundGamePacket::RemoveEntities(p) => { - debug!("Got remove entities packet {p:?}"); - - let mut system_state: SystemState<( - Query<&mut EntityIdIndex>, - Query<&mut LoadedBy>, - )> = SystemState::new(ecs); - - let (mut query, mut entity_query) = system_state.get_mut(ecs); - let Ok(mut entity_id_index) = query.get_mut(player_entity) else { - warn!("our local player doesn't have EntityIdIndex"); - continue; - }; - - for &id in &p.entity_ids { - let Some(entity) = entity_id_index.remove(id) else { - debug!( - "Tried to remove entity with id {id} but it wasn't in the EntityIdIndex" - ); - continue; - }; - let Ok(mut loaded_by) = entity_query.get_mut(entity) else { - warn!( - "tried to despawn entity {id} but it doesn't have a LoadedBy component", - ); - continue; - }; - - // the [`remove_despawned_entities_from_indexes`] system will despawn the entity - // if it's not loaded by anything anymore - - // also we can't just ecs.despawn because if we're in a swarm then the entity - // might still be loaded by another client - - loaded_by.remove(&player_entity); - } - } - ClientboundGamePacket::PlayerChat(p) => { - debug!("Got player chat packet {p:?}"); - - let mut system_state: SystemState> = - SystemState::new(ecs); - let mut chat_events = system_state.get_mut(ecs); - - chat_events.send(ChatReceivedEvent { - entity: player_entity, - packet: ChatPacket::Player(Arc::new(p.clone())), - }); - } - ClientboundGamePacket::SystemChat(p) => { - debug!("Got system chat packet {p:?}"); - - let mut system_state: SystemState> = - SystemState::new(ecs); - let mut chat_events = system_state.get_mut(ecs); - - chat_events.send(ChatReceivedEvent { - entity: player_entity, - packet: ChatPacket::System(Arc::new(p.clone())), - }); - } - ClientboundGamePacket::DisguisedChat(p) => { - debug!("Got disguised chat packet {p:?}"); - - let mut system_state: SystemState> = - SystemState::new(ecs); - let mut chat_events = system_state.get_mut(ecs); - - chat_events.send(ChatReceivedEvent { - entity: player_entity, - packet: ChatPacket::Disguised(Arc::new(p.clone())), - }); - } - ClientboundGamePacket::Sound(_p) => { - // debug!("Got sound packet {p:?}"); - } - ClientboundGamePacket::LevelEvent(p) => { - debug!("Got level event packet {p:?}"); - } - ClientboundGamePacket::BlockUpdate(p) => { - debug!("Got block update packet {p:?}"); - - let mut system_state: SystemState> = - SystemState::new(ecs); - let mut query = system_state.get_mut(ecs); - let local_player = query.get_mut(player_entity).unwrap(); - - let world = local_player.instance.write(); - - world.chunks.set_block_state(&p.pos, p.block_state); - } - ClientboundGamePacket::Animate(p) => { - debug!("Got animate packet {p:?}"); - } - ClientboundGamePacket::SectionBlocksUpdate(p) => { - debug!("Got section blocks update packet {p:?}"); - let mut system_state: SystemState> = - SystemState::new(ecs); - let mut query = system_state.get_mut(ecs); - let local_player = query.get_mut(player_entity).unwrap(); - - let world = local_player.instance.write(); - - for state in &p.states { - world - .chunks - .set_block_state(&(p.section_pos + state.pos), state.state); - } - } - ClientboundGamePacket::GameEvent(p) => { - use azalea_protocol::packets::game::c_game_event::EventType; - - debug!("Got game event packet {p:?}"); - - #[allow(clippy::single_match)] - match p.event { - EventType::ChangeGameMode => { - let mut system_state: SystemState> = - SystemState::new(ecs); - let mut query = system_state.get_mut(ecs); - let mut local_game_mode = query.get_mut(player_entity).unwrap(); - if let Some(new_game_mode) = GameMode::from_id(p.param as u8) { - local_game_mode.current = new_game_mode; - } - } - _ => {} - } - } - ClientboundGamePacket::LevelParticles(p) => { - debug!("Got level particles packet {p:?}"); - } - ClientboundGamePacket::ServerData(p) => { - debug!("Got server data packet {p:?}"); - } - ClientboundGamePacket::SetEquipment(p) => { - debug!("Got set equipment packet {p:?}"); - } - ClientboundGamePacket::UpdateMobEffect(p) => { - debug!("Got update mob effect packet {p:?}"); - } - ClientboundGamePacket::AddExperienceOrb(_) => {} - ClientboundGamePacket::AwardStats(_) => {} - ClientboundGamePacket::BlockChangedAck(_) => {} - ClientboundGamePacket::BlockDestruction(_) => {} - ClientboundGamePacket::BlockEntityData(_) => {} - ClientboundGamePacket::BlockEvent(p) => { - debug!("Got block event packet {p:?}"); - } - ClientboundGamePacket::BossEvent(_) => {} - ClientboundGamePacket::CommandSuggestions(_) => {} - ClientboundGamePacket::ContainerSetContent(p) => { - debug!("Got container set content packet {p:?}"); - - let mut system_state: SystemState<( - Query<&mut Inventory>, - EventWriter, - )> = SystemState::new(ecs); - let (mut query, mut events) = system_state.get_mut(ecs); - let mut inventory = query.get_mut(player_entity).unwrap(); - - // container id 0 is always the player's inventory - if p.container_id == 0 { - // this is just so it has the same type as the `else` block - for (i, slot) in p.items.iter().enumerate() { - if let Some(slot_mut) = inventory.inventory_menu.slot_mut(i) { - *slot_mut = slot.clone(); - } - } - } else { - events.send(SetContainerContentEvent { - entity: player_entity, - slots: p.items.clone(), - container_id: p.container_id, - }); - } - } - ClientboundGamePacket::ContainerSetData(p) => { - debug!("Got container set data packet {p:?}"); - // let mut system_state: SystemState> = - // SystemState::new(ecs); - // let mut query = system_state.get_mut(ecs); - // let mut inventory = - // query.get_mut(player_entity).unwrap(); - - // TODO: handle ContainerSetData packet - // this is used for various things like the furnace progress - // bar - // see https://wiki.vg/Protocol#Set_Container_Property - } - ClientboundGamePacket::ContainerSetSlot(p) => { - debug!("Got container set slot packet {p:?}"); - - let mut system_state: SystemState> = SystemState::new(ecs); - let mut query = system_state.get_mut(ecs); - let mut inventory = query.get_mut(player_entity).unwrap(); - - if p.container_id == -1 { - // -1 means carried item - inventory.carried = p.item_stack.clone(); - } else if p.container_id == -2 { - if let Some(slot) = inventory.inventory_menu.slot_mut(p.slot.into()) { - *slot = p.item_stack.clone(); - } - } else { - let is_creative_mode_and_inventory_closed = false; - // technically minecraft has slightly different behavior here if you're in - // creative mode and have your inventory open - if p.container_id == 0 - && azalea_inventory::Player::is_hotbar_slot(p.slot.into()) - { - // minecraft also sets a "pop time" here which is used for an animation - // but that's not really necessary - if let Some(slot) = inventory.inventory_menu.slot_mut(p.slot.into()) { - *slot = p.item_stack.clone(); - } - } else if p.container_id == inventory.id - && (p.container_id != 0 || !is_creative_mode_and_inventory_closed) - { - // var2.containerMenu.setItem(var4, var1.getStateId(), var3); - if let Some(slot) = inventory.menu_mut().slot_mut(p.slot.into()) { - *slot = p.item_stack.clone(); - inventory.state_id = p.state_id; - } - } - } - } - ClientboundGamePacket::ContainerClose(_p) => { - // there's p.container_id but minecraft doesn't actually check it - let mut system_state: SystemState> = - SystemState::new(ecs); - let mut client_side_close_container_events = system_state.get_mut(ecs); - client_side_close_container_events.send(ClientSideCloseContainerEvent { - entity: player_entity, - }); - } - ClientboundGamePacket::Cooldown(_) => {} - ClientboundGamePacket::CustomChatCompletions(_) => {} - ClientboundGamePacket::DeleteChat(_) => {} - ClientboundGamePacket::Explode(p) => { - trace!("Got explode packet {p:?}"); - if let Some(knockback) = p.knockback { - let mut system_state: SystemState> = - SystemState::new(ecs); - let mut knockback_events = system_state.get_mut(ecs); - - knockback_events.send(KnockbackEvent { - entity: player_entity, - knockback: KnockbackType::Set(knockback), - }); - - system_state.apply(ecs); - } - } - ClientboundGamePacket::ForgetLevelChunk(p) => { - debug!("Got forget level chunk packet {p:?}"); - - let mut system_state: SystemState> = - SystemState::new(ecs); - let mut query = system_state.get_mut(ecs); - let local_player = query.get_mut(player_entity).unwrap(); - - let mut partial_instance = local_player.partial_instance.write(); - - partial_instance.chunks.limited_set(&p.pos, None); - } - ClientboundGamePacket::HorseScreenOpen(_) => {} - ClientboundGamePacket::MapItemData(_) => {} - ClientboundGamePacket::MerchantOffers(_) => {} - ClientboundGamePacket::MoveVehicle(_) => {} - ClientboundGamePacket::OpenBook(_) => {} - ClientboundGamePacket::OpenScreen(p) => { - debug!("Got open screen packet {p:?}"); - let mut system_state: SystemState> = - SystemState::new(ecs); - let mut menu_opened_events = system_state.get_mut(ecs); - menu_opened_events.send(MenuOpenedEvent { - entity: player_entity, - window_id: p.container_id, - menu_type: p.menu_type, - title: p.title.to_owned(), - }); - } - ClientboundGamePacket::OpenSignEditor(_) => {} - ClientboundGamePacket::Ping(p) => { - debug!("Got ping packet {p:?}"); - - let mut system_state: SystemState> = - SystemState::new(ecs); - let mut send_packet_events = system_state.get_mut(ecs); - - send_packet_events.send(SendPacketEvent::new( - player_entity, - ServerboundPong { id: p.id }, - )); - } - ClientboundGamePacket::PlaceGhostRecipe(_) => {} - ClientboundGamePacket::PlayerCombatEnd(_) => {} - ClientboundGamePacket::PlayerCombatEnter(_) => {} - ClientboundGamePacket::PlayerCombatKill(p) => { - debug!("Got player kill packet {p:?}"); - - #[allow(clippy::type_complexity)] - let mut system_state: SystemState<( - Commands, - Query<(&MinecraftEntityId, Option<&Dead>)>, - EventWriter, - )> = SystemState::new(ecs); - let (mut commands, mut query, mut death_events) = system_state.get_mut(ecs); - let (entity_id, dead) = query.get_mut(player_entity).unwrap(); - - if *entity_id == p.player_id && dead.is_none() { - commands.entity(player_entity).insert(Dead); - death_events.send(DeathEvent { - entity: player_entity, - packet: Some(p.clone()), - }); - } - - system_state.apply(ecs); - } - ClientboundGamePacket::PlayerLookAt(_) => {} - ClientboundGamePacket::RemoveMobEffect(_) => {} - ClientboundGamePacket::ResourcePackPush(p) => { - debug!("Got resource pack packet {p:?}"); - - let mut system_state: SystemState> = - SystemState::new(ecs); - let mut resource_pack_events = system_state.get_mut(ecs); - - resource_pack_events.send(ResourcePackEvent { - entity: player_entity, - id: p.id, - url: p.url.to_owned(), - hash: p.hash.to_owned(), - required: p.required, - prompt: p.prompt.to_owned(), - }); - - system_state.apply(ecs); - } - ClientboundGamePacket::ResourcePackPop(_) => {} - ClientboundGamePacket::Respawn(p) => { - debug!("Got respawn packet {p:?}"); - - #[allow(clippy::type_complexity)] - let mut system_state: SystemState<( - Commands, - Query<( - &mut InstanceHolder, - &GameProfileComponent, - &ClientInformation, - )>, - EventWriter, - ResMut, - )> = SystemState::new(ecs); - let (mut commands, mut query, mut instance_loaded_events, mut instance_container) = - system_state.get_mut(ecs); - let (mut instance_holder, game_profile, client_information) = - query.get_mut(player_entity).unwrap(); - - { - let new_instance_name = p.common.dimension.clone(); - - let Some((_dimension_type, dimension_data)) = p - .common - .dimension_type(&instance_holder.instance.read().registries) - else { - continue; - }; - - // add this world to the instance_container (or don't if it's already - // there) - let weak_instance = instance_container.insert( - new_instance_name.clone(), - dimension_data.height, - dimension_data.min_y, - &instance_holder.instance.read().registries, - ); - instance_loaded_events.send(InstanceLoadedEvent { - entity: player_entity, - name: new_instance_name.clone(), - instance: Arc::downgrade(&weak_instance), - }); - - // set the partial_world to an empty world - // (when we add chunks or entities those will be in the - // instance_container) - - *instance_holder.partial_instance.write() = PartialInstance::new( - azalea_world::chunk_storage::calculate_chunk_storage_range( - client_information.view_distance.into(), - ), - Some(player_entity), - ); - instance_holder.instance = weak_instance; - - // this resets a bunch of our components like physics and stuff - let entity_bundle = EntityBundle::new( - game_profile.uuid, - Vec3::default(), - azalea_registry::EntityKind::Player, - new_instance_name, - ); - // update the local gamemode and metadata things - commands.entity(player_entity).insert(( - LocalGameMode { - current: p.common.game_type, - previous: p.common.previous_game_type.into(), - }, - entity_bundle, - )); - } - - // Remove the Dead marker component from the player. - commands.entity(player_entity).remove::(); - - system_state.apply(ecs); - } - - ClientboundGamePacket::StartConfiguration(_p) => { - let mut system_state: SystemState<(Commands, EventWriter)> = - SystemState::new(ecs); - let (mut commands, mut packet_events) = system_state.get_mut(ecs); - - packet_events.send(SendPacketEvent::new( - player_entity, - ServerboundConfigurationAcknowledged {}, - )); - - commands - .entity(player_entity) - .insert(crate::client::InConfigState) - .remove::(); - - system_state.apply(ecs); - } - - ClientboundGamePacket::EntityPositionSync(p) => { - let mut system_state: SystemState<( - Commands, - Query<(&EntityIdIndex, &InstanceHolder)>, - )> = SystemState::new(ecs); - let (mut commands, mut query) = system_state.get_mut(ecs); - let (entity_id_index, instance_holder) = query.get_mut(player_entity).unwrap(); - - let Some(entity) = entity_id_index.get(p.id) else { - debug!("Got teleport entity packet for unknown entity id {}", p.id); - continue; - }; - - let new_position = p.values.pos; - let new_on_ground = p.on_ground; - let new_look_direction = p.values.look_direction; - - commands.entity(entity).queue(RelativeEntityUpdate { - partial_world: instance_holder.partial_instance.clone(), - update: Box::new(move |entity_mut| { - let is_local_entity = entity_mut.get::().is_some(); - let mut physics = entity_mut.get_mut::().unwrap(); - - physics.vec_delta_codec.set_base(new_position); - - if is_local_entity { - debug!("Ignoring entity position sync packet for local player"); - return; - } - - physics.set_on_ground(new_on_ground); - - let mut last_sent_position = - entity_mut.get_mut::().unwrap(); - **last_sent_position = new_position; - let mut position = entity_mut.get_mut::().unwrap(); - **position = new_position; - - let mut look_direction = entity_mut.get_mut::().unwrap(); - *look_direction = new_look_direction; - }), - }); - - system_state.apply(ecs); - } - - ClientboundGamePacket::SelectAdvancementsTab(_) => {} - ClientboundGamePacket::SetActionBarText(_) => {} - ClientboundGamePacket::SetBorderCenter(_) => {} - ClientboundGamePacket::SetBorderLerpSize(_) => {} - ClientboundGamePacket::SetBorderSize(_) => {} - ClientboundGamePacket::SetBorderWarningDelay(_) => {} - ClientboundGamePacket::SetBorderWarningDistance(_) => {} - ClientboundGamePacket::SetCamera(_) => {} - ClientboundGamePacket::SetDisplayObjective(_) => {} - ClientboundGamePacket::SetObjective(_) => {} - ClientboundGamePacket::SetPassengers(_) => {} - ClientboundGamePacket::SetPlayerTeam(_) => {} - ClientboundGamePacket::SetScore(_) => {} - ClientboundGamePacket::SetSimulationDistance(_) => {} - ClientboundGamePacket::SetSubtitleText(_) => {} - ClientboundGamePacket::SetTitleText(_) => {} - ClientboundGamePacket::SetTitlesAnimation(_) => {} - ClientboundGamePacket::ClearTitles(_) => {} - ClientboundGamePacket::SoundEntity(_) => {} - ClientboundGamePacket::StopSound(_) => {} - ClientboundGamePacket::TabList(_) => {} - ClientboundGamePacket::TagQuery(_) => {} - ClientboundGamePacket::TakeItemEntity(_) => {} - ClientboundGamePacket::BundleDelimiter(_) => {} - ClientboundGamePacket::DamageEvent(_) => {} - ClientboundGamePacket::HurtAnimation(_) => {} - - ClientboundGamePacket::TickingState(_) => {} - ClientboundGamePacket::TickingStep(_) => {} - - ClientboundGamePacket::ResetScore(_) => {} - ClientboundGamePacket::CookieRequest(_) => {} - ClientboundGamePacket::DebugSample(_) => {} - ClientboundGamePacket::PongResponse(_) => {} - ClientboundGamePacket::StoreCookie(_) => {} - ClientboundGamePacket::Transfer(_) => {} - ClientboundGamePacket::MoveMinecartAlongTrack(_) => {} - ClientboundGamePacket::SetHeldSlot(_) => {} - ClientboundGamePacket::SetPlayerInventory(_) => {} - ClientboundGamePacket::ProjectilePower(_) => {} - ClientboundGamePacket::CustomReportDetails(_) => {} - ClientboundGamePacket::ServerLinks(_) => {} - ClientboundGamePacket::PlayerRotation(_) => {} - ClientboundGamePacket::RecipeBookAdd(_) => {} - ClientboundGamePacket::RecipeBookRemove(_) => {} - ClientboundGamePacket::RecipeBookSettings(_) => {} - } - } -} - -/// An event for sending a packet to the server while we're in the `game` state. -#[derive(Event)] -pub struct SendPacketEvent { - pub sent_by: Entity, - pub packet: ServerboundGamePacket, -} -impl SendPacketEvent { - pub fn new(sent_by: Entity, packet: impl Packet) -> Self { - let packet = packet.into_variant(); - Self { sent_by, packet } - } -} - -pub fn handle_send_packet_event( - mut send_packet_events: EventReader, - mut query: Query<&mut RawConnection>, -) { - for event in send_packet_events.read() { - if let Ok(raw_connection) = query.get_mut(event.sent_by) { - // debug!("Sending packet: {:?}", event.packet); - if let Err(e) = raw_connection.write_packet(event.packet.clone()) { - error!("Failed to send packet: {e}"); - } - } - } -} diff --git a/azalea-client/src/packet_handling/login.rs b/azalea-client/src/packet_handling/login.rs deleted file mode 100644 index 8cf45afc..00000000 --- a/azalea-client/src/packet_handling/login.rs +++ /dev/null @@ -1,114 +0,0 @@ -// login packets aren't actually handled here because compression/encryption -// would make packet handling a lot messier - -use std::{collections::HashSet, sync::Arc}; - -use azalea_protocol::packets::{ - Packet, - login::{ - ClientboundLoginPacket, ServerboundLoginPacket, - s_custom_query_answer::ServerboundCustomQueryAnswer, - }, -}; -use bevy_ecs::{prelude::*, system::SystemState}; -use derive_more::{Deref, DerefMut}; -use tokio::sync::mpsc; -use tracing::error; - -// this struct is defined here anyways though so it's consistent with the other -// ones - -/// An event that's sent when we receive a login packet from the server. Note -/// that if you want to handle this in a system, you must add -/// `.before(azalea::packet_handling::login::process_packet_events)` to it -/// because that system clears the events. -#[derive(Event, Debug, Clone)] -pub struct LoginPacketEvent { - /// The client entity that received the packet. - pub entity: Entity, - /// The packet that was actually received. - pub packet: Arc, -} - -/// Event for sending a login packet to the server. -#[derive(Event)] -pub struct SendLoginPacketEvent { - pub entity: Entity, - pub packet: ServerboundLoginPacket, -} -impl SendLoginPacketEvent { - pub fn new(entity: Entity, packet: impl Packet) -> Self { - let packet = packet.into_variant(); - Self { entity, packet } - } -} - -#[derive(Component)] -pub struct LoginSendPacketQueue { - pub tx: mpsc::UnboundedSender, -} - -/// A marker component for local players that are currently in the -/// `login` state. -#[derive(Component, Clone, Debug)] -pub struct InLoginState; - -pub fn handle_send_packet_event( - mut send_packet_events: EventReader, - mut query: Query<&mut LoginSendPacketQueue>, -) { - for event in send_packet_events.read() { - if let Ok(queue) = query.get_mut(event.entity) { - let _ = queue.tx.send(event.packet.clone()); - } else { - error!("Sent SendPacketEvent for entity that doesn't have a LoginSendPacketQueue"); - } - } -} - -/// Plugins can add to this set if they want to handle a custom query packet -/// themselves. This component removed after the login state ends. -#[derive(Component, Default, Debug, Deref, DerefMut)] -pub struct IgnoreQueryIds(HashSet); - -pub fn process_packet_events(ecs: &mut World) { - let mut events_owned = Vec::new(); - let mut system_state: SystemState>> = SystemState::new(ecs); - let mut events = system_state.get_mut(ecs); - for LoginPacketEvent { - entity: player_entity, - packet, - } in events.drain() - { - // we do this so `ecs` isn't borrowed for the whole loop - events_owned.push((player_entity, packet)); - } - for (player_entity, packet) in events_owned { - #[allow(clippy::single_match)] - match packet.as_ref() { - ClientboundLoginPacket::CustomQuery(p) => { - let mut system_state: SystemState<( - EventWriter, - Query<&IgnoreQueryIds>, - )> = SystemState::new(ecs); - let (mut send_packet_events, query) = system_state.get_mut(ecs); - - let ignore_query_ids = query.get(player_entity).ok().map(|x| x.0.clone()); - if let Some(ignore_query_ids) = ignore_query_ids { - if ignore_query_ids.contains(&p.transaction_id) { - continue; - } - } - - send_packet_events.send(SendLoginPacketEvent::new( - player_entity, - ServerboundCustomQueryAnswer { - transaction_id: p.transaction_id, - data: None, - }, - )); - } - _ => {} - } - } -} diff --git a/azalea-client/src/packet_handling/mod.rs b/azalea-client/src/packet_handling/mod.rs deleted file mode 100644 index 908f368e..00000000 --- a/azalea-client/src/packet_handling/mod.rs +++ /dev/null @@ -1,78 +0,0 @@ -use azalea_entity::{EntityUpdateSet, metadata::Health}; -use bevy_app::{App, First, Plugin, PreUpdate, Update}; -use bevy_ecs::prelude::*; - -use self::{ - game::{ - AddPlayerEvent, DeathEvent, InstanceLoadedEvent, KeepAliveEvent, RemovePlayerEvent, - ResourcePackEvent, UpdatePlayerEvent, - }, - login::{LoginPacketEvent, SendLoginPacketEvent}, -}; -use crate::{chat::ChatReceivedEvent, events::death_listener}; - -pub mod configuration; -pub mod game; -pub mod login; - -pub struct PacketHandlerPlugin; - -pub fn death_event_on_0_health( - query: Query<(Entity, &Health), Changed>, - mut death_events: EventWriter, -) { - for (entity, health) in query.iter() { - if **health == 0. { - death_events.send(DeathEvent { - entity, - packet: None, - }); - } - } -} - -impl Plugin for PacketHandlerPlugin { - fn build(&self, app: &mut App) { - app.add_systems( - First, - (game::send_packet_events, configuration::send_packet_events), - ) - .add_systems( - PreUpdate, - ( - game::process_packet_events - // we want to index and deindex right after - .before(EntityUpdateSet::Deindex), - configuration::process_packet_events, - login::handle_send_packet_event, - login::process_packet_events, - ), - ) - .add_systems( - Update, - ( - ( - configuration::handle_send_packet_event, - game::handle_send_packet_event, - ) - .chain(), - death_event_on_0_health.before(death_listener), - ), - ) - // we do this instead of add_event so we can handle the events ourselves - .init_resource::>() - .init_resource::>() - .add_event::() - .add_event::() - .add_event::() - .add_event::() - .add_event::() - .add_event::() - .add_event::() - .add_event::() - .add_event::() - .add_event::() - .add_event::() - .add_event::(); - } -} diff --git a/azalea-client/src/player.rs b/azalea-client/src/player.rs index f0641cf1..0940255c 100755 --- a/azalea-client/src/player.rs +++ b/azalea-client/src/player.rs @@ -8,7 +8,7 @@ use bevy_ecs::{ }; use uuid::Uuid; -use crate::{GameProfileComponent, packet_handling::game::AddPlayerEvent}; +use crate::{GameProfileComponent, packet::game::AddPlayerEvent}; /// A player in the tab list. #[derive(Debug, Clone)] diff --git a/azalea-client/src/plugins/attack.rs b/azalea-client/src/plugins/attack.rs new file mode 100644 index 00000000..1b2bc1ee --- /dev/null +++ b/azalea-client/src/plugins/attack.rs @@ -0,0 +1,149 @@ +use azalea_core::{game_type::GameMode, tick::GameTick}; +use azalea_entity::{ + Attributes, Physics, + metadata::{ShiftKeyDown, Sprinting}, + update_bounding_box, +}; +use azalea_physics::PhysicsSet; +use azalea_protocol::packets::game::s_interact::{self, ServerboundInteract}; +use azalea_world::MinecraftEntityId; +use bevy_app::{App, Plugin, Update}; +use bevy_ecs::prelude::*; +use derive_more::{Deref, DerefMut}; + +use super::packet::game::SendPacketEvent; +use crate::{ + Client, interact::SwingArmEvent, local_player::LocalGameMode, movement::MoveEventsSet, + respawn::perform_respawn, +}; + +pub struct AttackPlugin; +impl Plugin for AttackPlugin { + fn build(&self, app: &mut App) { + app.add_event::() + .add_systems( + Update, + handle_attack_event + .before(update_bounding_box) + .before(MoveEventsSet) + .after(perform_respawn), + ) + .add_systems( + GameTick, + ( + increment_ticks_since_last_attack, + update_attack_strength_scale.after(PhysicsSet), + ) + .chain(), + ); + } +} + +impl Client { + /// Attack the entity with the given id. + pub fn attack(&mut self, entity_id: MinecraftEntityId) { + self.ecs.lock().send_event(AttackEvent { + entity: self.entity, + target: entity_id, + }); + } + + /// Whether the player has an attack cooldown. + pub fn has_attack_cooldown(&self) -> bool { + let Some(AttackStrengthScale(ticks_since_last_attack)) = + self.get_component::() + else { + // they don't even have an AttackStrengthScale so they probably can't attack + // lmao, just return false + return false; + }; + ticks_since_last_attack < 1.0 + } +} + +#[derive(Event)] +pub struct AttackEvent { + pub entity: Entity, + pub target: MinecraftEntityId, +} +pub fn handle_attack_event( + mut events: EventReader, + mut query: Query<( + &LocalGameMode, + &mut TicksSinceLastAttack, + &mut Physics, + &mut Sprinting, + &mut ShiftKeyDown, + )>, + mut send_packet_events: EventWriter, + mut swing_arm_event: EventWriter, +) { + for event in events.read() { + let (game_mode, mut ticks_since_last_attack, mut physics, mut sprinting, sneaking) = + query.get_mut(event.entity).unwrap(); + + swing_arm_event.send(SwingArmEvent { + entity: event.entity, + }); + send_packet_events.send(SendPacketEvent::new( + event.entity, + ServerboundInteract { + entity_id: event.target, + action: s_interact::ActionType::Attack, + using_secondary_action: **sneaking, + }, + )); + + // we can't attack if we're in spectator mode but it still sends the attack + // packet + if game_mode.current == GameMode::Spectator { + continue; + }; + + ticks_since_last_attack.0 = 0; + + physics.velocity = physics.velocity.multiply(0.6, 1.0, 0.6); + **sprinting = false; + } +} + +#[derive(Default, Bundle)] +pub struct AttackBundle { + pub ticks_since_last_attack: TicksSinceLastAttack, + pub attack_strength_scale: AttackStrengthScale, +} + +#[derive(Default, Component, Clone, Deref, DerefMut)] +pub struct TicksSinceLastAttack(pub u32); +pub fn increment_ticks_since_last_attack(mut query: Query<&mut TicksSinceLastAttack>) { + for mut ticks_since_last_attack in query.iter_mut() { + **ticks_since_last_attack += 1; + } +} + +#[derive(Default, Component, Clone, Deref, DerefMut)] +pub struct AttackStrengthScale(pub f32); +pub fn update_attack_strength_scale( + mut query: Query<(&TicksSinceLastAttack, &Attributes, &mut AttackStrengthScale)>, +) { + for (ticks_since_last_attack, attributes, mut attack_strength_scale) in query.iter_mut() { + // look 0.5 ticks into the future because that's what vanilla does + **attack_strength_scale = + get_attack_strength_scale(ticks_since_last_attack.0, attributes, 0.5); + } +} + +/// Returns how long it takes for the attack cooldown to reset (in ticks). +pub fn get_attack_strength_delay(attributes: &Attributes) -> f32 { + ((1. / attributes.attack_speed.calculate()) * 20.) as f32 +} + +pub fn get_attack_strength_scale( + ticks_since_last_attack: u32, + attributes: &Attributes, + in_ticks: f32, +) -> f32 { + let attack_strength_delay = get_attack_strength_delay(attributes); + let attack_strength = (ticks_since_last_attack as f32 + in_ticks) / attack_strength_delay; + attack_strength.clamp(0., 1.) +} diff --git a/azalea-client/src/plugins/brand.rs b/azalea-client/src/plugins/brand.rs new file mode 100644 index 00000000..e15a6c67 --- /dev/null +++ b/azalea-client/src/plugins/brand.rs @@ -0,0 +1,63 @@ +use azalea_buf::AzaleaWrite; +use azalea_core::resource_location::ResourceLocation; +use azalea_protocol::{ + common::client_information::ClientInformation, + packets::config::{ + s_client_information::ServerboundClientInformation, + s_custom_payload::ServerboundCustomPayload, + }, +}; +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; +use tracing::{debug, warn}; + +use super::packet::config::SendConfigPacketEvent; +use crate::packet::login::InLoginState; + +pub struct BrandPlugin; +impl Plugin for BrandPlugin { + fn build(&self, app: &mut App) { + app.add_systems( + Update, + handle_end_login_state.before(crate::packet::config::handle_send_packet_event), + ); + } +} + +fn handle_end_login_state( + mut removed: RemovedComponents, + query: Query<&ClientInformation>, + mut send_packet_events: EventWriter, +) { + for entity in removed.read() { + let mut brand_data = Vec::new(); + // azalea pretends to be vanilla everywhere else so it makes sense to lie here + // too + "vanilla".azalea_write(&mut brand_data).unwrap(); + send_packet_events.send(SendConfigPacketEvent::new( + entity, + ServerboundCustomPayload { + identifier: ResourceLocation::new("brand"), + data: brand_data.into(), + }, + )); + + let client_information = match query.get(entity).ok() { + Some(i) => i, + None => { + warn!( + "ClientInformation component was not set before leaving login state, using a default" + ); + &ClientInformation::default() + } + }; + + debug!("Writing ClientInformation while in config state: {client_information:?}"); + send_packet_events.send(SendConfigPacketEvent::new( + entity, + ServerboundClientInformation { + information: client_information.clone(), + }, + )); + } +} diff --git a/azalea-client/src/plugins/chat/handler.rs b/azalea-client/src/plugins/chat/handler.rs new file mode 100644 index 00000000..d598acdb --- /dev/null +++ b/azalea-client/src/plugins/chat/handler.rs @@ -0,0 +1,61 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use azalea_protocol::packets::{ + game::{s_chat::LastSeenMessagesUpdate, ServerboundChat, ServerboundChatCommand}, + Packet, +}; +use bevy_ecs::prelude::*; + +use super::ChatKind; +use crate::packet::game::SendPacketEvent; + +/// Send a chat packet to the server of a specific kind (chat message or +/// command). Usually you just want [`SendChatEvent`] instead. +/// +/// Usually setting the kind to `Message` will make it send a chat message even +/// if it starts with a slash, but some server implementations will always do a +/// command if it starts with a slash. +/// +/// If you're wondering why this isn't two separate events, it's so ordering is +/// preserved if multiple chat messages and commands are sent at the same time. +#[derive(Event)] +pub struct SendChatKindEvent { + pub entity: Entity, + pub content: String, + pub kind: ChatKind, +} + +pub fn handle_send_chat_kind_event( + mut events: EventReader, + mut send_packet_events: EventWriter, +) { + for event in events.read() { + let content = event + .content + .chars() + .filter(|c| !matches!(c, '\x00'..='\x1F' | '\x7F' | 'ยง')) + .take(256) + .collect::(); + let packet = match event.kind { + ChatKind::Message => ServerboundChat { + message: content, + timestamp: SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time shouldn't be before epoch") + .as_millis() + .try_into() + .expect("Instant should fit into a u64"), + salt: azalea_crypto::make_salt(), + signature: None, + last_seen_messages: LastSeenMessagesUpdate::default(), + } + .into_variant(), + ChatKind::Command => { + // TODO: chat signing + ServerboundChatCommand { command: content }.into_variant() + } + }; + + send_packet_events.send(SendPacketEvent::new(event.entity, packet)); + } +} diff --git a/azalea-client/src/plugins/chat/mod.rs b/azalea-client/src/plugins/chat/mod.rs new file mode 100644 index 00000000..66c77b56 --- /dev/null +++ b/azalea-client/src/plugins/chat/mod.rs @@ -0,0 +1,240 @@ +//! Implementations of chat-related features. + +pub mod handler; + +use std::sync::Arc; + +use azalea_chat::FormattedText; +use azalea_protocol::packets::game::{ + c_disguised_chat::ClientboundDisguisedChat, c_player_chat::ClientboundPlayerChat, + c_system_chat::ClientboundSystemChat, +}; +use bevy_app::{App, Plugin, Update}; +use bevy_ecs::{ + entity::Entity, + event::{EventReader, EventWriter}, + prelude::Event, + schedule::IntoSystemConfigs, +}; +use handler::{SendChatKindEvent, handle_send_chat_kind_event}; +use uuid::Uuid; + +use super::packet::game::handle_outgoing_packets; +use crate::client::Client; + +pub struct ChatPlugin; +impl Plugin for ChatPlugin { + fn build(&self, app: &mut App) { + app.add_event::() + .add_event::() + .add_event::() + .add_systems( + Update, + ( + handle_send_chat_event, + handle_send_chat_kind_event.after(handle_outgoing_packets), + ) + .chain(), + ); + } +} + +/// A chat packet, either a system message or a chat message. +#[derive(Debug, Clone, PartialEq)] +pub enum ChatPacket { + System(Arc), + Player(Arc), + Disguised(Arc), +} + +macro_rules! regex { + ($re:literal $(,)?) => {{ + static RE: std::sync::LazyLock = + std::sync::LazyLock::new(|| regex::Regex::new($re).unwrap()); + &RE + }}; +} + +impl ChatPacket { + /// Get the message shown in chat for this packet. + pub fn message(&self) -> FormattedText { + match self { + ChatPacket::System(p) => p.content.clone(), + ChatPacket::Player(p) => p.message(), + ChatPacket::Disguised(p) => p.message(), + } + } + + /// Determine the username of the sender and content of the message. This + /// does not preserve formatting codes. If it's not a player-sent chat + /// message or the sender couldn't be determined, the username part will be + /// None. + pub fn split_sender_and_content(&self) -> (Option, String) { + match self { + ChatPacket::System(p) => { + let message = p.content.to_string(); + // Overlay messages aren't in chat + if p.overlay { + return (None, message); + } + // It's a system message, so we'll have to match the content + // with regex + if let Some(m) = regex!("^<([a-zA-Z_0-9]{1,16})> (.+)$").captures(&message) { + return (Some(m[1].to_string()), m[2].to_string()); + } + + (None, message) + } + ChatPacket::Player(p) => ( + // If it's a player chat packet, then the sender and content + // are already split for us. + Some(p.chat_type.name.to_string()), + p.body.content.clone(), + ), + ChatPacket::Disguised(p) => ( + // disguised chat packets are basically the same as player chat packets but without + // the chat signing things + Some(p.chat_type.name.to_string()), + p.message.to_string(), + ), + } + } + + /// Get the username of the sender of the message. If it's not a + /// player-sent chat message or the sender couldn't be determined, this + /// will be None. + pub fn username(&self) -> Option { + self.split_sender_and_content().0 + } + + /// Get the UUID of the sender of the message. If it's not a + /// player-sent chat message, this will be None (this is sometimes the case + /// when a server uses a plugin to modify chat messages). + pub fn uuid(&self) -> Option { + match self { + ChatPacket::System(_) => None, + ChatPacket::Player(m) => Some(m.sender), + ChatPacket::Disguised(_) => None, + } + } + + /// Get the content part of the message as a string. This does not preserve + /// formatting codes. If it's not a player-sent chat message or the sender + /// couldn't be determined, this will contain the entire message. + pub fn content(&self) -> String { + self.split_sender_and_content().1 + } + + /// Create a new Chat from a string. This is meant to be used as a + /// convenience function for testing. + pub fn new(message: &str) -> Self { + ChatPacket::System(Arc::new(ClientboundSystemChat { + content: FormattedText::from(message), + overlay: false, + })) + } + + /// Whether this message was sent with /msg (or aliases). It works by + /// checking the translation key, so it won't work on servers that use their + /// own whisper system. + pub fn is_whisper(&self) -> bool { + match self.message() { + FormattedText::Text(_) => false, + FormattedText::Translatable(t) => t.key == "commands.message.display.incoming", + } + } +} + +impl Client { + /// Send a chat message to the server. This only sends the chat packet and + /// not the command packet, which means on some servers you can use this to + /// send chat messages that start with a `/`. The [`Client::chat`] function + /// handles checking whether the message is a command and using the + /// proper packet for you, so you should use that instead. + pub fn send_chat_packet(&self, message: &str) { + self.ecs.lock().send_event(SendChatKindEvent { + entity: self.entity, + content: message.to_string(), + kind: ChatKind::Message, + }); + self.run_schedule_sender.send(()).unwrap(); + } + + /// Send a command packet to the server. The `command` argument should not + /// include the slash at the front. + /// + /// You can also just use [`Client::chat`] and start your message with a `/` + /// to send a command. + pub fn send_command_packet(&self, command: &str) { + self.ecs.lock().send_event(SendChatKindEvent { + entity: self.entity, + content: command.to_string(), + kind: ChatKind::Command, + }); + self.run_schedule_sender.send(()).unwrap(); + } + + /// Send a message in chat. + /// + /// ```rust,no_run + /// # use azalea_client::Client; + /// # async fn example(bot: Client) -> anyhow::Result<()> { + /// bot.chat("Hello, world!"); + /// # Ok(()) + /// # } + /// ``` + pub fn chat(&self, content: &str) { + self.ecs.lock().send_event(SendChatEvent { + entity: self.entity, + content: content.to_string(), + }); + self.run_schedule_sender.send(()).unwrap(); + } +} + +/// A client received a chat message packet. +#[derive(Event, Debug, Clone)] +pub struct ChatReceivedEvent { + pub entity: Entity, + pub packet: ChatPacket, +} + +/// Send a chat message (or command, if it starts with a slash) to the server. +#[derive(Event)] +pub struct SendChatEvent { + pub entity: Entity, + pub content: String, +} + +pub fn handle_send_chat_event( + mut events: EventReader, + mut send_chat_kind_events: EventWriter, +) { + for event in events.read() { + if event.content.starts_with('/') { + send_chat_kind_events.send(SendChatKindEvent { + entity: event.entity, + content: event.content[1..].to_string(), + kind: ChatKind::Command, + }); + } else { + send_chat_kind_events.send(SendChatKindEvent { + entity: event.entity, + content: event.content.clone(), + kind: ChatKind::Message, + }); + } + } +} + +/// A kind of chat packet, either a chat message or a command. +pub enum ChatKind { + Message, + Command, +} + +// TODO +// MessageSigner, ChatMessageContent, LastSeenMessages +// fn sign_message() -> MessageSignature { +// MessageSignature::default() +// } diff --git a/azalea-client/src/plugins/chunks.rs b/azalea-client/src/plugins/chunks.rs new file mode 100644 index 00000000..cdda3eba --- /dev/null +++ b/azalea-client/src/plugins/chunks.rs @@ -0,0 +1,223 @@ +//! Used for Minecraft's chunk batching introduced in 23w31a (1.20.2). It's used +//! for making the server spread out how often it sends us chunk packets +//! depending on our receiving speed. + +use std::{ + io::Cursor, + ops::Deref, + time::{Duration, Instant}, +}; + +use azalea_core::position::ChunkPos; +use azalea_protocol::packets::game::{ + c_level_chunk_with_light::ClientboundLevelChunkWithLight, + s_chunk_batch_received::ServerboundChunkBatchReceived, +}; +use bevy_app::{App, Plugin, Update}; +use bevy_ecs::prelude::*; +use simdnbt::owned::BaseNbt; +use tracing::{error, trace}; + +use super::packet::game::handle_outgoing_packets; +use crate::{ + InstanceHolder, interact::handle_block_interact_event, inventory::InventorySet, + packet::game::SendPacketEvent, respawn::perform_respawn, +}; + +pub struct ChunksPlugin; +impl Plugin for ChunksPlugin { + fn build(&self, app: &mut App) { + app.add_systems( + Update, + ( + handle_chunk_batch_start_event, + handle_receive_chunk_events, + handle_chunk_batch_finished_event, + ) + .chain() + .before(handle_outgoing_packets) + .before(InventorySet) + .before(handle_block_interact_event) + .before(perform_respawn), + ) + .add_event::() + .add_event::() + .add_event::(); + } +} + +#[derive(Event)] +pub struct ReceiveChunkEvent { + pub entity: Entity, + pub packet: ClientboundLevelChunkWithLight, +} + +#[derive(Component, Clone, Debug)] +pub struct ChunkBatchInfo { + pub start_time: Instant, + pub aggregated_duration_per_chunk: Duration, + pub old_samples_weight: u32, +} + +#[derive(Event)] +pub struct ChunkBatchStartEvent { + pub entity: Entity, +} +#[derive(Event)] +pub struct ChunkBatchFinishedEvent { + pub entity: Entity, + pub batch_size: u32, +} + +pub fn handle_receive_chunk_events( + mut events: EventReader, + mut query: Query<&mut InstanceHolder>, +) { + for event in events.read() { + let pos = ChunkPos::new(event.packet.x, event.packet.z); + + let local_player = query.get_mut(event.entity).unwrap(); + + let mut instance = local_player.instance.write(); + let mut partial_instance = local_player.partial_instance.write(); + + // OPTIMIZATION: if we already know about the chunk from the shared world (and + // not ourselves), then we don't need to parse it again. This is only used when + // we have a shared world, since we check that the chunk isn't currently owned + // by this client. + let shared_chunk = instance.chunks.get(&pos); + let this_client_has_chunk = partial_instance.chunks.limited_get(&pos).is_some(); + + if !this_client_has_chunk { + if let Some(shared_chunk) = shared_chunk { + trace!("Skipping parsing chunk {pos:?} because we already know about it"); + partial_instance + .chunks + .limited_set(&pos, Some(shared_chunk)); + continue; + } + } + + let heightmaps_nbt = &event.packet.chunk_data.heightmaps; + // necessary to make the unwrap_or work + let empty_nbt = BaseNbt::default(); + let heightmaps = heightmaps_nbt.unwrap_or(&empty_nbt).deref(); + + if let Err(e) = partial_instance.chunks.replace_with_packet_data( + &pos, + &mut Cursor::new(&event.packet.chunk_data.data), + heightmaps, + &mut instance.chunks, + ) { + error!( + "Couldn't set chunk data: {e}. World height: {}", + instance.chunks.height + ); + } + } +} + +impl ChunkBatchInfo { + pub fn batch_finished(&mut self, batch_size: u32) { + if batch_size == 0 { + return; + } + let batch_duration = self.start_time.elapsed(); + let duration_per_chunk = batch_duration / batch_size; + let clamped_duration = Duration::clamp( + duration_per_chunk, + self.aggregated_duration_per_chunk / 3, + self.aggregated_duration_per_chunk * 3, + ); + self.aggregated_duration_per_chunk = + ((self.aggregated_duration_per_chunk * self.old_samples_weight) + clamped_duration) + / (self.old_samples_weight + 1); + self.old_samples_weight = u32::min(49, self.old_samples_weight + 1); + } + + pub fn desired_chunks_per_tick(&self) -> f32 { + (7000000. / self.aggregated_duration_per_chunk.as_nanos() as f64) as f32 + } +} + +pub fn handle_chunk_batch_start_event( + mut query: Query<&mut ChunkBatchInfo>, + mut events: EventReader, +) { + for event in events.read() { + if let Ok(mut chunk_batch_info) = query.get_mut(event.entity) { + chunk_batch_info.start_time = Instant::now(); + } + } +} + +pub fn handle_chunk_batch_finished_event( + mut query: Query<&mut ChunkBatchInfo>, + mut events: EventReader, + mut send_packets: EventWriter, +) { + for event in events.read() { + if let Ok(mut chunk_batch_info) = query.get_mut(event.entity) { + chunk_batch_info.batch_finished(event.batch_size); + let desired_chunks_per_tick = chunk_batch_info.desired_chunks_per_tick(); + send_packets.send(SendPacketEvent::new( + event.entity, + ServerboundChunkBatchReceived { + desired_chunks_per_tick, + }, + )); + } + } +} + +#[derive(Clone, Debug)] +pub struct ChunkReceiveSpeedAccumulator { + batch_sizes: Vec, + /// as milliseconds + batch_durations: Vec, + index: usize, + filled_size: usize, +} +impl ChunkReceiveSpeedAccumulator { + pub fn new(capacity: usize) -> Self { + Self { + batch_sizes: vec![0; capacity], + batch_durations: vec![0; capacity], + index: 0, + filled_size: 0, + } + } + + pub fn accumulate(&mut self, batch_size: u32, batch_duration: Duration) { + self.batch_sizes[self.index] = batch_size; + self.batch_durations[self.index] = + f32::clamp(batch_duration.as_millis() as f32, 0., 15000.) as u32; + self.index = (self.index + 1) % self.batch_sizes.len(); + if self.filled_size < self.batch_sizes.len() { + self.filled_size += 1; + } + } + + pub fn get_millis_per_chunk(&self) -> f64 { + let mut total_batch_size = 0; + let mut total_batch_duration = 0; + for i in 0..self.filled_size { + total_batch_size += self.batch_sizes[i]; + total_batch_duration += self.batch_durations[i]; + } + if total_batch_size == 0 { + return 0.; + } + total_batch_duration as f64 / total_batch_size as f64 + } +} + +impl Default for ChunkBatchInfo { + fn default() -> Self { + Self { + start_time: Instant::now(), + aggregated_duration_per_chunk: Duration::from_millis(2), + old_samples_weight: 1, + } + } +} diff --git a/azalea-client/src/plugins/disconnect.rs b/azalea-client/src/plugins/disconnect.rs new file mode 100644 index 00000000..bd10ac75 --- /dev/null +++ b/azalea-client/src/plugins/disconnect.rs @@ -0,0 +1,103 @@ +//! Disconnect a client from the server. + +use azalea_chat::FormattedText; +use azalea_entity::{EntityBundle, InLoadedChunk, LocalEntity, metadata::PlayerMetadataBundle}; +use bevy_app::{App, Plugin, PostUpdate}; +use bevy_ecs::{ + component::Component, + entity::Entity, + event::{EventReader, EventWriter}, + prelude::Event, + query::{Changed, With}, + schedule::IntoSystemConfigs, + system::{Commands, Query}, +}; +use derive_more::Deref; +use tracing::trace; + +use crate::{ + InstanceHolder, client::JoinedClientBundle, events::LocalPlayerEvents, + raw_connection::RawConnection, +}; + +pub struct DisconnectPlugin; +impl Plugin for DisconnectPlugin { + fn build(&self, app: &mut App) { + app.add_event::().add_systems( + PostUpdate, + ( + update_read_packets_task_running_component, + disconnect_on_connection_dead, + remove_components_from_disconnected_players, + ) + .chain(), + ); + } +} + +/// An event sent when a client is getting disconnected. +#[derive(Event)] +pub struct DisconnectEvent { + pub entity: Entity, + pub reason: Option, +} + +/// A system that removes the several components from our clients when they get +/// a [`DisconnectEvent`]. +pub fn remove_components_from_disconnected_players( + mut commands: Commands, + mut events: EventReader, + mut loaded_by_query: Query<&mut azalea_entity::LoadedBy>, +) { + for DisconnectEvent { entity, .. } in events.read() { + trace!("Got DisconnectEvent for {entity:?}"); + commands + .entity(*entity) + .remove::() + .remove::() + .remove::() + .remove::() + .remove::() + // this makes it close the tcp connection + .remove::() + // swarm detects when this tx gets dropped to fire SwarmEvent::Disconnect + .remove::(); + // note that we don't remove the client from the ECS, so if they decide + // to reconnect they'll keep their state + + // now we have to remove ourselves from the LoadedBy for every entity. + // in theory this could be inefficient if we have massive swarms... but in + // practice this is fine. + for mut loaded_by in &mut loaded_by_query.iter_mut() { + loaded_by.remove(entity); + } + } +} + +#[derive(Component, Clone, Copy, Debug, Deref)] +pub struct IsConnectionAlive(bool); + +fn update_read_packets_task_running_component( + query: Query<(Entity, &RawConnection)>, + mut commands: Commands, +) { + for (entity, raw_connection) in &query { + let running = raw_connection.is_alive(); + commands.entity(entity).insert(IsConnectionAlive(running)); + } +} + +#[allow(clippy::type_complexity)] +fn disconnect_on_connection_dead( + query: Query<(Entity, &IsConnectionAlive), (Changed, With)>, + mut disconnect_events: EventWriter, +) { + for (entity, &is_connection_alive) in &query { + if !*is_connection_alive { + disconnect_events.send(DisconnectEvent { + entity, + reason: None, + }); + } + } +} diff --git a/azalea-client/src/plugins/events.rs b/azalea-client/src/plugins/events.rs new file mode 100644 index 00000000..3d34d75f --- /dev/null +++ b/azalea-client/src/plugins/events.rs @@ -0,0 +1,259 @@ +//! Defines the [`Event`] enum and makes those events trigger when they're sent +//! in the ECS. + +use std::sync::Arc; + +use azalea_chat::FormattedText; +use azalea_core::tick::GameTick; +use azalea_entity::Dead; +use azalea_protocol::packets::game::{ + ClientboundGamePacket, c_player_combat_kill::ClientboundPlayerCombatKill, +}; +use azalea_world::{InstanceName, MinecraftEntityId}; +use bevy_app::{App, Plugin, PreUpdate, Update}; +use bevy_ecs::{ + component::Component, + event::EventReader, + query::{Added, With}, + schedule::IntoSystemConfigs, + system::Query, +}; +use derive_more::{Deref, DerefMut}; +use tokio::sync::mpsc; + +use crate::{ + PlayerInfo, + chat::{ChatPacket, ChatReceivedEvent}, + disconnect::DisconnectEvent, + packet::game::{ + AddPlayerEvent, DeathEvent, KeepAliveEvent, ReceivePacketEvent, RemovePlayerEvent, + UpdatePlayerEvent, + }, +}; + +// (for contributors): +// HOW TO ADD A NEW (packet based) EVENT: +// - Add it as an ECS event first: +// - Make a struct that contains an entity field and some data fields (look +// in packet/game/events.rs for examples. These structs should always have +// their names end with "Event". +// - (the `entity` field is the local player entity that's receiving the +// event) +// - In the GamePacketHandler, you always have a `player` field that you can +// use. +// - Add the event struct in PacketPlugin::build +// - (in the `impl Plugin for PacketPlugin`) +// - To get the event writer, you have to get an EventWriter. +// Look at other packets in packet/game/mod.rs for examples. +// +// At this point, you've created a new ECS event. That's annoying for bots to +// use though, so you might wanna add it to the Event enum too: +// - In this file, add a new variant to that Event enum with the same name +// as your event (without the "Event" suffix). +// - Create a new system function like the other ones here, and put that +// system function in the `impl Plugin for EventsPlugin` + +/// Something that happened in-game, such as a tick passing or chat message +/// being sent. +/// +/// Note: Events are sent before they're processed, so for example game ticks +/// happen at the beginning of a tick before anything has happened. +#[derive(Debug, Clone)] +pub enum Event { + /// Happens right after the bot switches into the Game state, but before + /// it's actually spawned. This can be useful for setting the client + /// information with `Client::set_client_information`, so the packet + /// doesn't have to be sent twice. + /// + /// You may want to use [`Event::Login`] instead to wait for the bot to be + /// in the world. + Init, + /// The client is now in the world. Fired when we receive a login packet. + Login, + /// A chat message was sent in the game chat. + Chat(ChatPacket), + /// Happens 20 times per second, but only when the world is loaded. + Tick, + /// We received a packet from the server. + /// + /// ``` + /// # use azalea_client::Event; + /// # use azalea_protocol::packets::game::ClientboundGamePacket; + /// # async fn example(event: Event) { + /// # match event { + /// Event::Packet(packet) => match *packet { + /// ClientboundGamePacket::Login(_) => { + /// println!("login packet"); + /// } + /// _ => {} + /// }, + /// # _ => {} + /// # } + /// # } + /// ``` + Packet(Arc), + /// A player joined the game (or more specifically, was added to the tab + /// list). + AddPlayer(PlayerInfo), + /// A player left the game (or maybe is still in the game and was just + /// removed from the tab list). + RemovePlayer(PlayerInfo), + /// A player was updated in the tab list (gamemode, display + /// name, or latency changed). + UpdatePlayer(PlayerInfo), + /// The client player died in-game. + Death(Option>), + /// A `KeepAlive` packet was sent by the server. + KeepAlive(u64), + /// The client disconnected from the server. + Disconnect(Option), +} + +/// A component that contains an event sender for events that are only +/// received by local players. The receiver for this is returned by +/// [`Client::start_client`]. +/// +/// [`Client::start_client`]: crate::Client::start_client +#[derive(Component, Deref, DerefMut)] +pub struct LocalPlayerEvents(pub mpsc::UnboundedSender); + +pub struct EventsPlugin; +impl Plugin for EventsPlugin { + fn build(&self, app: &mut App) { + app.add_systems( + Update, + ( + chat_listener, + login_listener, + packet_listener, + add_player_listener, + update_player_listener, + remove_player_listener, + keepalive_listener, + death_listener, + disconnect_listener, + ), + ) + .add_systems( + PreUpdate, + init_listener.before(crate::packet::game::process_packet_events), + ) + .add_systems(GameTick, tick_listener); + } +} + +// when LocalPlayerEvents is added, it means the client just started +pub fn init_listener(query: Query<&LocalPlayerEvents, Added>) { + for local_player_events in &query { + let _ = local_player_events.send(Event::Init); + } +} + +// when MinecraftEntityId is added, it means the player is now in the world +pub fn login_listener(query: Query<&LocalPlayerEvents, Added>) { + for local_player_events in &query { + let _ = local_player_events.send(Event::Login); + } +} + +pub fn chat_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader) { + for event in events.read() { + let local_player_events = query + .get(event.entity) + .expect("Non-local entities shouldn't be able to receive chat events"); + let _ = local_player_events.send(Event::Chat(event.packet.clone())); + } +} + +// only tick if we're in a world +pub fn tick_listener(query: Query<&LocalPlayerEvents, With>) { + for local_player_events in &query { + let _ = local_player_events.send(Event::Tick); + } +} + +pub fn packet_listener( + query: Query<&LocalPlayerEvents>, + mut events: EventReader, +) { + for event in events.read() { + let local_player_events = query + .get(event.entity) + .expect("Non-local entities shouldn't be able to receive packet events"); + let _ = local_player_events.send(Event::Packet(event.packet.clone())); + } +} + +pub fn add_player_listener( + query: Query<&LocalPlayerEvents>, + mut events: EventReader, +) { + for event in events.read() { + let local_player_events = query + .get(event.entity) + .expect("Non-local entities shouldn't be able to receive add player events"); + let _ = local_player_events.send(Event::AddPlayer(event.info.clone())); + } +} + +pub fn update_player_listener( + query: Query<&LocalPlayerEvents>, + mut events: EventReader, +) { + for event in events.read() { + let local_player_events = query + .get(event.entity) + .expect("Non-local entities shouldn't be able to receive update player events"); + let _ = local_player_events.send(Event::UpdatePlayer(event.info.clone())); + } +} + +pub fn remove_player_listener( + query: Query<&LocalPlayerEvents>, + mut events: EventReader, +) { + for event in events.read() { + let local_player_events = query + .get(event.entity) + .expect("Non-local entities shouldn't be able to receive remove player events"); + let _ = local_player_events.send(Event::RemovePlayer(event.info.clone())); + } +} + +pub fn death_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader) { + for event in events.read() { + if let Ok(local_player_events) = query.get(event.entity) { + let _ = local_player_events.send(Event::Death(event.packet.clone().map(|p| p.into()))); + } + } +} + +/// Send the "Death" event for [`LocalEntity`]s that died with no reason. +pub fn dead_component_listener(query: Query<&LocalPlayerEvents, Added>) { + for local_player_events in &query { + local_player_events.send(Event::Death(None)).unwrap(); + } +} + +pub fn keepalive_listener( + query: Query<&LocalPlayerEvents>, + mut events: EventReader, +) { + for event in events.read() { + let local_player_events = query + .get(event.entity) + .expect("Non-local entities shouldn't be able to receive keepalive events"); + let _ = local_player_events.send(Event::KeepAlive(event.id)); + } +} + +pub fn disconnect_listener( + query: Query<&LocalPlayerEvents>, + mut events: EventReader, +) { + for event in events.read() { + if let Ok(local_player_events) = query.get(event.entity) { + let _ = local_player_events.send(Event::Disconnect(event.reason.clone())); + } + } +} diff --git a/azalea-client/src/plugins/interact.rs b/azalea-client/src/plugins/interact.rs new file mode 100644 index 00000000..1a344cc8 --- /dev/null +++ b/azalea-client/src/plugins/interact.rs @@ -0,0 +1,370 @@ +use std::ops::AddAssign; + +use azalea_block::BlockState; +use azalea_core::{ + block_hit_result::BlockHitResult, + direction::Direction, + game_type::GameMode, + position::{BlockPos, Vec3}, +}; +use azalea_entity::{ + Attributes, EyeHeight, LocalEntity, LookDirection, Position, clamp_look_direction, view_vector, +}; +use azalea_inventory::{ItemStack, ItemStackData, components}; +use azalea_physics::clip::{BlockShapeType, ClipContext, FluidPickType}; +use azalea_protocol::packets::game::{ + s_interact::InteractionHand, + s_swing::ServerboundSwing, + s_use_item_on::{BlockHit, ServerboundUseItemOn}, +}; +use azalea_world::{Instance, InstanceContainer, InstanceName}; +use bevy_app::{App, Plugin, Update}; +use bevy_ecs::{ + component::Component, + entity::Entity, + event::{Event, EventReader, EventWriter}, + query::{Changed, With}, + schedule::IntoSystemConfigs, + system::{Commands, Query, Res}, +}; +use derive_more::{Deref, DerefMut}; +use tracing::warn; + +use super::packet::game::handle_outgoing_packets; +use crate::{ + Client, + attack::handle_attack_event, + inventory::{Inventory, InventorySet}, + local_player::{LocalGameMode, PermissionLevel, PlayerAbilities}, + movement::MoveEventsSet, + packet::game::SendPacketEvent, + respawn::perform_respawn, +}; + +/// A plugin that allows clients to interact with blocks in the world. +pub struct InteractPlugin; +impl Plugin for InteractPlugin { + fn build(&self, app: &mut App) { + app.add_event::() + .add_event::() + .add_systems( + Update, + ( + ( + update_hit_result_component.after(clamp_look_direction), + handle_block_interact_event, + handle_swing_arm_event, + ) + .before(handle_outgoing_packets) + .after(InventorySet) + .after(perform_respawn) + .after(handle_attack_event) + .chain(), + update_modifiers_for_held_item + .after(InventorySet) + .after(MoveEventsSet), + ), + ); + } +} + +impl Client { + /// Right click a block. The behavior of this depends on the target block, + /// and it'll either place the block you're holding in your hand or use the + /// block you clicked (like toggling a lever). + /// + /// Note that this may trigger anticheats as it doesn't take into account + /// whether you're actually looking at the block. + pub fn block_interact(&mut self, position: BlockPos) { + self.ecs.lock().send_event(BlockInteractEvent { + entity: self.entity, + position, + }); + } +} + +/// Right click a block. The behavior of this depends on the target block, +/// and it'll either place the block you're holding in your hand or use the +/// block you clicked (like toggling a lever). +#[derive(Event)] +pub struct BlockInteractEvent { + /// The local player entity that's opening the container. + pub entity: Entity, + /// The coordinates of the container. + pub position: BlockPos, +} + +/// A component that contains the number of changes this client has made to +/// blocks. +#[derive(Component, Copy, Clone, Debug, Default, Deref)] +pub struct CurrentSequenceNumber(u32); + +impl AddAssign 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); + +pub fn handle_block_interact_event( + mut events: EventReader, + mut query: Query<(Entity, &mut CurrentSequenceNumber, &HitResultComponent)>, + mut send_packet_events: EventWriter, +) { + for event in events.read() { + let Ok((entity, mut sequence_number, hit_result)) = query.get_mut(event.entity) else { + warn!("Sent BlockInteractEvent for entity that doesn't have the required components"); + continue; + }; + + // TODO: check to make sure we're within the world border + + *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 + + // the block_hit data will depend on whether we're looking at the block and + // whether we can reach it + + let block_hit = if hit_result.block_pos == event.position { + // we're looking at the block :) + BlockHit { + block_pos: hit_result.block_pos, + direction: hit_result.direction, + location: hit_result.location, + inside: hit_result.inside, + world_border: hit_result.world_border, + } + } else { + // we're not looking at the block, so make up some numbers + BlockHit { + block_pos: event.position, + direction: Direction::Up, + location: event.position.center(), + inside: false, + world_border: false, + } + }; + + send_packet_events.send(SendPacketEvent::new( + entity, + ServerboundUseItemOn { + hand: InteractionHand::MainHand, + block_hit, + sequence: sequence_number.0, + }, + )); + } +} + +#[allow(clippy::type_complexity)] +pub fn update_hit_result_component( + mut commands: Commands, + mut query: Query<( + Entity, + Option<&mut HitResultComponent>, + &LocalGameMode, + &Position, + &EyeHeight, + &LookDirection, + &InstanceName, + )>, + instance_container: Res, +) { + for (entity, hit_result_ref, game_mode, position, eye_height, look_direction, world_name) in + &mut query + { + let pick_range = if game_mode.current == GameMode::Creative { + 6. + } else { + 4.5 + }; + let eye_position = Vec3 { + x: position.x, + y: position.y + **eye_height as f64, + z: position.z, + }; + + let Some(instance_lock) = instance_container.get(world_name) else { + continue; + }; + let instance = instance_lock.read(); + + let hit_result = pick(look_direction, &eye_position, &instance.chunks, pick_range); + if let Some(mut hit_result_ref) = hit_result_ref { + **hit_result_ref = hit_result; + } else { + commands + .entity(entity) + .insert(HitResultComponent(hit_result)); + } + } +} + +/// Get the block that a player would be looking at if their eyes were at the +/// given direction and position. +/// +/// If you need to get the block the player is looking at right now, use +/// [`HitResultComponent`]. +pub fn pick( + look_direction: &LookDirection, + eye_position: &Vec3, + chunks: &azalea_world::ChunkStorage, + pick_range: f64, +) -> BlockHitResult { + let view_vector = view_vector(look_direction); + let end_position = eye_position + &(view_vector * pick_range); + azalea_physics::clip::clip( + chunks, + ClipContext { + from: *eye_position, + to: end_position, + block_shape_type: BlockShapeType::Outline, + fluid_pick_type: FluidPickType::None, + }, + ) +} + +/// Whether we can't interact with the block, based on your gamemode. If +/// this is false, then we can interact with the block. +/// +/// Passing the inventory, block position, and instance is necessary for the +/// adventure mode check. +pub fn check_is_interaction_restricted( + instance: &Instance, + block_pos: &BlockPos, + game_mode: &GameMode, + inventory: &Inventory, +) -> bool { + match game_mode { + GameMode::Adventure => { + // vanilla checks for abilities.mayBuild here but servers have no + // way of modifying that + + let held_item = inventory.held_item(); + match &held_item { + ItemStack::Present(item) => { + let block = instance.chunks.get_block_state(block_pos); + let Some(block) = block else { + // block isn't loaded so just say that it is restricted + return true; + }; + check_block_can_be_broken_by_item_in_adventure_mode(item, &block) + } + _ => true, + } + } + GameMode::Spectator => true, + _ => false, + } +} + +/// Check if the item has the `CanDestroy` tag for the block. +pub fn check_block_can_be_broken_by_item_in_adventure_mode( + item: &ItemStackData, + _block: &BlockState, +) -> bool { + // minecraft caches the last checked block but that's kind of an unnecessary + // optimization and makes the code too complicated + + if !item.components.has::() { + // no CanDestroy tag + return false; + }; + + false + + // for block_predicate in can_destroy { + // // TODO + // // defined in BlockPredicateArgument.java + // } + + // 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, +} +pub fn handle_swing_arm_event( + mut events: EventReader, + mut send_packet_events: EventWriter, +) { + for event in events.read() { + send_packet_events.send(SendPacketEvent::new( + event.entity, + ServerboundSwing { + hand: InteractionHand::MainHand, + }, + )); + } +} + +#[allow(clippy::type_complexity)] +fn update_modifiers_for_held_item( + mut query: Query<(&mut Attributes, &Inventory), (With, Changed)>, +) { + for (mut attributes, inventory) in &mut query { + let held_item = inventory.held_item(); + + use azalea_registry::Item; + let added_attack_speed = match held_item.kind() { + Item::WoodenSword => -2.4, + Item::WoodenShovel => -3.0, + Item::WoodenPickaxe => -2.8, + Item::WoodenAxe => -3.2, + Item::WoodenHoe => -3.0, + + Item::StoneSword => -2.4, + Item::StoneShovel => -3.0, + Item::StonePickaxe => -2.8, + Item::StoneAxe => -3.2, + Item::StoneHoe => -2.0, + + Item::GoldenSword => -2.4, + Item::GoldenShovel => -3.0, + Item::GoldenPickaxe => -2.8, + Item::GoldenAxe => -3.0, + Item::GoldenHoe => -3.0, + + Item::IronSword => -2.4, + Item::IronShovel => -3.0, + Item::IronPickaxe => -2.8, + Item::IronAxe => -3.1, + Item::IronHoe => -1.0, + + Item::DiamondSword => -2.4, + Item::DiamondShovel => -3.0, + Item::DiamondPickaxe => -2.8, + Item::DiamondAxe => -3.0, + Item::DiamondHoe => 0.0, + + Item::NetheriteSword => -2.4, + Item::NetheriteShovel => -3.0, + Item::NetheritePickaxe => -2.8, + Item::NetheriteAxe => -3.0, + Item::NetheriteHoe => 0.0, + + Item::Trident => -2.9, + _ => 0., + }; + attributes + .attack_speed + .insert(azalea_entity::attributes::base_attack_speed_modifier( + added_attack_speed, + )); + } +} diff --git a/azalea-client/src/plugins/inventory.rs b/azalea-client/src/plugins/inventory.rs new file mode 100644 index 00000000..3f823ca2 --- /dev/null +++ b/azalea-client/src/plugins/inventory.rs @@ -0,0 +1,766 @@ +use std::collections::{HashMap, HashSet}; + +use azalea_chat::FormattedText; +pub use azalea_inventory::*; +use azalea_inventory::{ + item::MaxStackSizeExt, + operations::{ + ClickOperation, CloneClick, PickupAllClick, PickupClick, QuickCraftKind, QuickCraftStatus, + QuickCraftStatusKind, QuickMoveClick, ThrowClick, + }, +}; +use azalea_protocol::packets::game::{ + s_container_click::ServerboundContainerClick, s_container_close::ServerboundContainerClose, + s_set_carried_item::ServerboundSetCarriedItem, +}; +use azalea_registry::MenuKind; +use bevy_app::{App, Plugin, Update}; +use bevy_ecs::{ + component::Component, + entity::Entity, + event::EventReader, + prelude::{Event, EventWriter}, + schedule::{IntoSystemConfigs, SystemSet}, + system::Query, +}; +use tracing::warn; + +use super::packet::game::handle_outgoing_packets; +use crate::{ + Client, local_player::PlayerAbilities, packet::game::SendPacketEvent, respawn::perform_respawn, +}; + +pub struct InventoryPlugin; +impl Plugin for InventoryPlugin { + fn build(&self, app: &mut App) { + app.add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_systems( + Update, + ( + handle_set_selected_hotbar_slot_event, + handle_menu_opened_event, + handle_set_container_content_event, + handle_container_click_event, + handle_container_close_event.before(handle_outgoing_packets), + handle_client_side_close_container_event, + ) + .chain() + .in_set(InventorySet) + .before(perform_respawn), + ); + } +} + +#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)] +pub struct InventorySet; + +impl Client { + /// Return the menu that is currently open. If no menu is open, this will + /// have the player's inventory. + pub fn menu(&self) -> Menu { + let mut ecs = self.ecs.lock(); + let inventory = self.query::<&Inventory>(&mut ecs); + inventory.menu().clone() + } +} + +/// A component present on all local players that have an inventory. +#[derive(Component, Debug, Clone)] +pub struct Inventory { + /// A component that contains the player's inventory menu. This is + /// guaranteed to be a `Menu::Player`. + /// + /// We keep it as a [`Menu`] since `Menu` has some useful functions that + /// bare [`azalea_inventory::Player`] doesn't have. + pub inventory_menu: azalea_inventory::Menu, + + /// The ID of the container that's currently open. Its value is not + /// guaranteed to be anything specific, and may change every time you open a + /// container (unless it's 0, in which case it means that no container is + /// open). + pub id: i32, + /// The current container menu that the player has open. If no container is + /// open, this will be `None`. + pub container_menu: Option, + /// The custom name of the menu that's currently open. This is Some when + /// `container_menu` is Some. + pub container_menu_title: Option, + /// The item that is currently held by the cursor. `Slot::Empty` if nothing + /// is currently being held. + /// + /// This is different from [`Self::selected_hotbar_slot`], which is the + /// item that's selected in the hotbar. + pub carried: ItemStack, + /// An identifier used by the server to track client inventory desyncs. This + /// is sent on every container click, and it's only ever updated when the + /// server sends a new container update. + pub state_id: u32, + + pub quick_craft_status: QuickCraftStatusKind, + pub quick_craft_kind: QuickCraftKind, + /// A set of the indexes of the slots that have been right clicked in + /// this "quick craft". + pub quick_craft_slots: HashSet, + + /// The index of the item in the hotbar that's currently being held by the + /// player. This MUST be in the range 0..9 (not including 9). + /// + /// In a vanilla client this is changed by pressing the number keys or using + /// the scroll wheel. + pub selected_hotbar_slot: u8, +} + +impl Inventory { + /// Returns a reference to the currently active menu. If a container is open + /// it'll return [`Self::container_menu`], otherwise + /// [`Self::inventory_menu`]. + /// + /// Use [`Self::menu_mut`] if you need a mutable reference. + pub fn menu(&self) -> &azalea_inventory::Menu { + match &self.container_menu { + Some(menu) => menu, + _ => &self.inventory_menu, + } + } + + /// Returns a mutable reference to the currently active menu. If a container + /// is open it'll return [`Self::container_menu`], otherwise + /// [`Self::inventory_menu`]. + /// + /// Use [`Self::menu`] if you don't need a mutable reference. + pub fn menu_mut(&mut self) -> &mut azalea_inventory::Menu { + match &mut self.container_menu { + Some(menu) => menu, + _ => &mut self.inventory_menu, + } + } + + /// Modify the inventory as if the given operation was performed on it. + pub fn simulate_click( + &mut self, + operation: &ClickOperation, + player_abilities: &PlayerAbilities, + ) { + if let ClickOperation::QuickCraft(quick_craft) = operation { + let last_quick_craft_status_tmp = self.quick_craft_status.clone(); + self.quick_craft_status = last_quick_craft_status_tmp.clone(); + let last_quick_craft_status = last_quick_craft_status_tmp; + + // no carried item, reset + if self.carried.is_empty() { + return self.reset_quick_craft(); + } + // if we were starting or ending, or now we aren't ending and the status + // changed, reset + if (last_quick_craft_status == QuickCraftStatusKind::Start + || last_quick_craft_status == QuickCraftStatusKind::End + || self.quick_craft_status != QuickCraftStatusKind::End) + && (self.quick_craft_status != last_quick_craft_status) + { + return self.reset_quick_craft(); + } + if self.quick_craft_status == QuickCraftStatusKind::Start { + self.quick_craft_kind = quick_craft.kind.clone(); + if self.quick_craft_kind == QuickCraftKind::Middle && player_abilities.instant_break + { + self.quick_craft_status = QuickCraftStatusKind::Add; + self.quick_craft_slots.clear(); + } else { + self.reset_quick_craft(); + } + return; + } + if let QuickCraftStatus::Add { slot } = quick_craft.status { + let slot_item = self.menu().slot(slot as usize); + if let Some(slot_item) = slot_item { + if let ItemStack::Present(carried) = &self.carried { + // minecraft also checks slot.may_place(carried) and + // menu.can_drag_to(slot) + // but they always return true so they're not relevant for us + if can_item_quick_replace(slot_item, &self.carried, true) + && (self.quick_craft_kind == QuickCraftKind::Right + || carried.count as usize > self.quick_craft_slots.len()) + { + self.quick_craft_slots.insert(slot); + } + } + } + return; + } + if self.quick_craft_status == QuickCraftStatusKind::End { + if !self.quick_craft_slots.is_empty() { + if self.quick_craft_slots.len() == 1 { + // if we only clicked one slot, then turn this + // QuickCraftClick into a PickupClick + let slot = *self.quick_craft_slots.iter().next().unwrap(); + self.reset_quick_craft(); + self.simulate_click( + &match self.quick_craft_kind { + QuickCraftKind::Left => { + PickupClick::Left { slot: Some(slot) }.into() + } + QuickCraftKind::Right => { + PickupClick::Left { slot: Some(slot) }.into() + } + QuickCraftKind::Middle => { + // idk just do nothing i guess + return; + } + }, + player_abilities, + ); + return; + } + + let ItemStack::Present(mut carried) = self.carried.clone() else { + // this should never happen + return self.reset_quick_craft(); + }; + + let mut carried_count = carried.count; + let mut quick_craft_slots_iter = self.quick_craft_slots.iter(); + + loop { + let mut slot: &ItemStack; + let mut slot_index: u16; + let mut item_stack: &ItemStack; + + loop { + let Some(&next_slot) = quick_craft_slots_iter.next() else { + carried.count = carried_count; + self.carried = ItemStack::Present(carried); + return self.reset_quick_craft(); + }; + + slot = self.menu().slot(next_slot as usize).unwrap(); + slot_index = next_slot; + item_stack = &self.carried; + + if slot.is_present() + && can_item_quick_replace(slot, item_stack, true) + // this always returns true in most cases + // && slot.may_place(item_stack) + && ( + self.quick_craft_kind == QuickCraftKind::Middle + || item_stack.count() >= self.quick_craft_slots.len() as i32 + ) + { + break; + } + } + + // get the ItemStackData for the slot + let ItemStack::Present(slot) = slot else { + unreachable!("the loop above requires the slot to be present to break") + }; + + // if self.can_drag_to(slot) { + let mut new_carried = carried.clone(); + let slot_item_count = slot.count; + get_quick_craft_slot_count( + &self.quick_craft_slots, + &self.quick_craft_kind, + &mut new_carried, + slot_item_count, + ); + let max_stack_size = i32::min( + new_carried.kind.max_stack_size(), + i32::min( + new_carried.kind.max_stack_size(), + slot.kind.max_stack_size(), + ), + ); + if new_carried.count > max_stack_size { + new_carried.count = max_stack_size; + } + + carried_count -= new_carried.count - slot_item_count; + // we have to inline self.menu_mut() here to avoid the borrow checker + // complaining + let menu = match &mut self.container_menu { + Some(menu) => menu, + _ => &mut self.inventory_menu, + }; + *menu.slot_mut(slot_index as usize).unwrap() = + ItemStack::Present(new_carried); + } + } + } else { + return self.reset_quick_craft(); + } + } + // the quick craft status should always be in start if we're not in quick craft + // mode + if self.quick_craft_status != QuickCraftStatusKind::Start { + return self.reset_quick_craft(); + } + + match operation { + // left clicking outside inventory + ClickOperation::Pickup(PickupClick::Left { slot: None }) => { + if self.carried.is_present() { + // vanilla has `player.drop`s but they're only used + // server-side + // they're included as comments here in case you want to adapt this for a server + // implementation + + // player.drop(self.carried, true); + self.carried = ItemStack::Empty; + } + } + ClickOperation::Pickup(PickupClick::Right { slot: None }) => { + if self.carried.is_present() { + let _item = self.carried.split(1); + // player.drop(item, true); + } + } + ClickOperation::Pickup( + PickupClick::Left { slot: Some(slot) } | PickupClick::Right { slot: Some(slot) }, + ) => { + let Some(slot_item) = self.menu().slot(*slot as usize) else { + return; + }; + let carried = &self.carried; + // vanilla does a check called tryItemClickBehaviourOverride + // here + // i don't understand it so i didn't implement it + match slot_item { + ItemStack::Empty => if carried.is_present() {}, + ItemStack::Present(_) => todo!(), + } + } + ClickOperation::QuickMove( + QuickMoveClick::Left { slot } | QuickMoveClick::Right { slot }, + ) => { + // in vanilla it also tests if QuickMove has a slot index of -999 + // but i don't think that's ever possible so it's not covered here + loop { + let new_slot_item = self.menu_mut().quick_move_stack(*slot as usize); + let slot_item = self.menu().slot(*slot as usize).unwrap(); + if new_slot_item.is_empty() || slot_item != &new_slot_item { + break; + } + } + } + ClickOperation::Swap(s) => { + let source_slot_index = s.source_slot as usize; + let target_slot_index = s.target_slot as usize; + + let Some(source_slot) = self.menu().slot(source_slot_index) else { + return; + }; + let Some(target_slot) = self.menu().slot(target_slot_index) else { + return; + }; + if source_slot.is_empty() && target_slot.is_empty() { + return; + } + + if target_slot.is_empty() { + if self.menu().may_pickup(source_slot_index) { + let source_slot = source_slot.clone(); + let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap(); + *target_slot = source_slot; + } + } else if source_slot.is_empty() { + let ItemStack::Present(target_item) = target_slot else { + unreachable!("target slot is not empty but is not present"); + }; + if self.menu().may_place(source_slot_index, target_item) { + // get the target_item but mutable + let source_max_stack_size = self.menu().max_stack_size(source_slot_index); + + let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap(); + let new_source_slot = target_slot.split(source_max_stack_size); + *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot; + } + } else if self.menu().may_pickup(source_slot_index) { + let ItemStack::Present(target_item) = target_slot else { + unreachable!("target slot is not empty but is not present"); + }; + if self.menu().may_place(source_slot_index, target_item) { + let source_max_stack = self.menu().max_stack_size(source_slot_index); + if target_slot.count() > source_max_stack as i32 { + // if there's more than the max stack size in the target slot + + let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap(); + let new_source_slot = target_slot.split(source_max_stack); + *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot; + // if !self.inventory_menu.add(new_source_slot) { + // player.drop(new_source_slot, true); + // } + } else { + // normal swap + let new_target_slot = source_slot.clone(); + let new_source_slot = target_slot.clone(); + + let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap(); + *target_slot = new_target_slot; + + let source_slot = self.menu_mut().slot_mut(source_slot_index).unwrap(); + *source_slot = new_source_slot; + } + } + } + } + ClickOperation::Clone(CloneClick { slot }) => { + if !player_abilities.instant_break || self.carried.is_present() { + return; + } + let Some(source_slot) = self.menu().slot(*slot as usize) else { + return; + }; + let ItemStack::Present(source_item) = source_slot else { + return; + }; + let mut new_carried = source_item.clone(); + new_carried.count = new_carried.kind.max_stack_size(); + self.carried = ItemStack::Present(new_carried); + } + ClickOperation::Throw(c) => { + if self.carried.is_present() { + return; + } + + let (ThrowClick::Single { slot: slot_index } + | ThrowClick::All { slot: slot_index }) = c; + let slot_index = *slot_index as usize; + + let Some(slot) = self.menu_mut().slot_mut(slot_index) else { + return; + }; + let ItemStack::Present(slot_item) = slot else { + return; + }; + + let dropping_count = match c { + ThrowClick::Single { .. } => 1, + ThrowClick::All { .. } => slot_item.count, + }; + + let _dropping = slot_item.split(dropping_count as u32); + // player.drop(dropping, true); + } + ClickOperation::PickupAll(PickupAllClick { + slot: source_slot_index, + reversed, + }) => { + let source_slot_index = *source_slot_index as usize; + + let source_slot = self.menu().slot(source_slot_index).unwrap(); + let target_slot = self.carried.clone(); + + if target_slot.is_empty() + || (source_slot.is_present() && self.menu().may_pickup(source_slot_index)) + { + return; + } + + let ItemStack::Present(target_slot_item) = &target_slot else { + unreachable!("target slot is not empty but is not present"); + }; + + for round in 0..2 { + let iterator: Box> = if *reversed { + Box::new((0..self.menu().len()).rev()) + } else { + Box::new(0..self.menu().len()) + }; + + for i in iterator { + if target_slot_item.count < target_slot_item.kind.max_stack_size() { + let checking_slot = self.menu().slot(i).unwrap(); + if let ItemStack::Present(checking_item) = checking_slot { + if can_item_quick_replace(checking_slot, &target_slot, true) + && self.menu().may_pickup(i) + && (round != 0 + || checking_item.count + != checking_item.kind.max_stack_size()) + { + // get the checking_slot and checking_item again but mutable + let checking_slot = self.menu_mut().slot_mut(i).unwrap(); + + let taken_item = + checking_slot.split(checking_slot.count() as u32); + + // now extend the carried item + let target_slot = &mut self.carried; + let ItemStack::Present(target_slot_item) = target_slot else { + unreachable!("target slot is not empty but is not present"); + }; + target_slot_item.count += taken_item.count(); + } + } + } + } + } + } + _ => {} + } + } + + fn reset_quick_craft(&mut self) { + self.quick_craft_status = QuickCraftStatusKind::Start; + self.quick_craft_slots.clear(); + } + + /// Get the item in the player's hotbar that is currently being held. + pub fn held_item(&self) -> ItemStack { + let inventory = &self.inventory_menu; + let hotbar_items = &inventory.slots()[inventory.hotbar_slots_range()]; + hotbar_items[self.selected_hotbar_slot as usize].clone() + } +} + +fn can_item_quick_replace( + target_slot: &ItemStack, + item: &ItemStack, + ignore_item_count: bool, +) -> bool { + let ItemStack::Present(target_slot) = target_slot else { + return false; + }; + let ItemStack::Present(item) = item else { + // i *think* this is what vanilla does + // not 100% sure lol probably doesn't matter though + return false; + }; + + if !item.is_same_item_and_components(target_slot) { + return false; + } + let count = target_slot.count as u16 + + if ignore_item_count { + 0 + } else { + item.count as u16 + }; + count <= item.kind.max_stack_size() as u16 +} + +fn get_quick_craft_slot_count( + quick_craft_slots: &HashSet, + quick_craft_kind: &QuickCraftKind, + item: &mut ItemStackData, + slot_item_count: i32, +) { + item.count = match quick_craft_kind { + QuickCraftKind::Left => item.count / quick_craft_slots.len() as i32, + QuickCraftKind::Right => 1, + QuickCraftKind::Middle => item.kind.max_stack_size(), + }; + item.count += slot_item_count; +} + +impl Default for Inventory { + fn default() -> Self { + Inventory { + inventory_menu: Menu::Player(azalea_inventory::Player::default()), + id: 0, + container_menu: None, + container_menu_title: None, + carried: ItemStack::Empty, + state_id: 0, + quick_craft_status: QuickCraftStatusKind::Start, + quick_craft_kind: QuickCraftKind::Middle, + quick_craft_slots: HashSet::new(), + selected_hotbar_slot: 0, + } + } +} + +/// Sent from the server when a menu (like a chest or crafting table) was +/// opened by the client. +#[derive(Event, Debug)] +pub struct MenuOpenedEvent { + pub entity: Entity, + pub window_id: i32, + pub menu_type: MenuKind, + pub title: FormattedText, +} +fn handle_menu_opened_event( + mut events: EventReader, + mut query: Query<&mut Inventory>, +) { + for event in events.read() { + let mut inventory = query.get_mut(event.entity).unwrap(); + inventory.id = event.window_id; + inventory.container_menu = Some(Menu::from_kind(event.menu_type)); + inventory.container_menu_title = Some(event.title.clone()); + } +} + +/// Tell the server that we want to close a container. +/// +/// Note that this is also sent when the client closes its own inventory, even +/// though there is no packet for opening its inventory. +#[derive(Event)] +pub struct CloseContainerEvent { + pub entity: Entity, + /// The ID of the container to close. 0 for the player's inventory. If this + /// is not the same as the currently open inventory, nothing will happen. + pub id: i32, +} +fn handle_container_close_event( + query: Query<(Entity, &Inventory)>, + mut events: EventReader, + mut client_side_events: EventWriter, + mut send_packet_events: EventWriter, +) { + for event in events.read() { + let (entity, inventory) = query.get(event.entity).unwrap(); + if event.id != inventory.id { + warn!( + "Tried to close container with ID {}, but the current container ID is {}", + event.id, inventory.id + ); + continue; + } + + send_packet_events.send(SendPacketEvent::new( + entity, + ServerboundContainerClose { + container_id: inventory.id, + }, + )); + client_side_events.send(ClientSideCloseContainerEvent { + entity: event.entity, + }); + } +} + +/// Close a container without notifying the server. +/// +/// Note that this also gets fired when we get a [`CloseContainerEvent`]. +#[derive(Event)] +pub struct ClientSideCloseContainerEvent { + pub entity: Entity, +} +pub fn handle_client_side_close_container_event( + mut events: EventReader, + mut query: Query<&mut Inventory>, +) { + for event in events.read() { + let mut inventory = query.get_mut(event.entity).unwrap(); + inventory.container_menu = None; + inventory.id = 0; + inventory.container_menu_title = None; + } +} + +#[derive(Event, Debug)] +pub struct ContainerClickEvent { + pub entity: Entity, + pub window_id: i32, + pub operation: ClickOperation, +} +pub fn handle_container_click_event( + mut query: Query<(Entity, &mut Inventory)>, + mut events: EventReader, + mut send_packet_events: EventWriter, +) { + for event in events.read() { + let (entity, mut inventory) = query.get_mut(event.entity).unwrap(); + if inventory.id != event.window_id { + warn!( + "Tried to click container with ID {}, but the current container ID is {}", + event.window_id, inventory.id + ); + continue; + } + + let menu = inventory.menu_mut(); + let old_slots = menu.slots().clone(); + + // menu.click(&event.operation); + + // see which slots changed after clicking and put them in the hashmap + // the server uses this to check if we desynced + let mut changed_slots: HashMap = HashMap::new(); + for (slot_index, old_slot) in old_slots.iter().enumerate() { + let new_slot = &menu.slots()[slot_index]; + if old_slot != new_slot { + changed_slots.insert(slot_index as u16, new_slot.clone()); + } + } + + send_packet_events.send(SendPacketEvent::new( + entity, + ServerboundContainerClick { + container_id: event.window_id, + state_id: inventory.state_id, + slot_num: event.operation.slot_num().map(|n| n as i16).unwrap_or(-999), + button_num: event.operation.button_num(), + click_type: event.operation.click_type(), + changed_slots, + carried_item: inventory.carried.clone(), + }, + )); + } +} + +/// Sent from the server when the contents of a container are replaced. Usually +/// triggered by the `ContainerSetContent` packet. +#[derive(Event)] +pub struct SetContainerContentEvent { + pub entity: Entity, + pub slots: Vec, + pub container_id: i32, +} +fn handle_set_container_content_event( + mut events: EventReader, + mut query: Query<&mut Inventory>, +) { + for event in events.read() { + let mut inventory = query.get_mut(event.entity).unwrap(); + + if event.container_id != inventory.id { + warn!( + "Tried to set container content with ID {}, but the current container ID is {}", + event.container_id, inventory.id + ); + continue; + } + + let menu = inventory.menu_mut(); + for (i, slot) in event.slots.iter().enumerate() { + if let Some(slot_mut) = menu.slot_mut(i) { + *slot_mut = slot.clone(); + } + } + } +} + +#[derive(Event)] +pub struct SetSelectedHotbarSlotEvent { + pub entity: Entity, + /// The hotbar slot to select. This should be in the range 0..=8. + pub slot: u8, +} +fn handle_set_selected_hotbar_slot_event( + mut events: EventReader, + mut send_packet_events: EventWriter, + mut query: Query<&mut Inventory>, +) { + for event in events.read() { + let mut inventory = query.get_mut(event.entity).unwrap(); + + // if the slot is already selected, don't send a packet + if inventory.selected_hotbar_slot == event.slot { + continue; + } + + inventory.selected_hotbar_slot = event.slot; + send_packet_events.send(SendPacketEvent::new( + event.entity, + ServerboundSetCarriedItem { + slot: event.slot as u16, + }, + )); + } +} 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::() + .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), + ); + } +} + +/// 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::(); + } + } +} + +/// 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 }); + } +} diff --git a/azalea-client/src/plugins/mod.rs b/azalea-client/src/plugins/mod.rs new file mode 100644 index 00000000..11794fb3 --- /dev/null +++ b/azalea-client/src/plugins/mod.rs @@ -0,0 +1,14 @@ +pub mod attack; +pub mod brand; +pub mod chat; +pub mod chunks; +pub mod disconnect; +pub mod events; +pub mod interact; +pub mod inventory; +pub mod mining; +pub mod movement; +pub mod packet; +pub mod respawn; +pub mod task_pool; +pub mod tick_end; diff --git a/azalea-client/src/plugins/movement.rs b/azalea-client/src/plugins/movement.rs new file mode 100644 index 00000000..17b92e65 --- /dev/null +++ b/azalea-client/src/plugins/movement.rs @@ -0,0 +1,580 @@ +use std::backtrace::Backtrace; + +use azalea_core::position::Vec3; +use azalea_core::tick::GameTick; +use azalea_entity::{Attributes, Jumping, metadata::Sprinting}; +use azalea_entity::{InLoadedChunk, LastSentPosition, LookDirection, Physics, Position}; +use azalea_physics::{PhysicsSet, ai_step}; +use azalea_protocol::packets::game::{ServerboundPlayerCommand, ServerboundPlayerInput}; +use azalea_protocol::packets::{ + Packet, + game::{ + s_move_player_pos::ServerboundMovePlayerPos, + s_move_player_pos_rot::ServerboundMovePlayerPosRot, + s_move_player_rot::ServerboundMovePlayerRot, + s_move_player_status_only::ServerboundMovePlayerStatusOnly, + }, +}; +use azalea_world::{MinecraftEntityId, MoveEntityError}; +use bevy_app::{App, Plugin, Update}; +use bevy_ecs::prelude::{Event, EventWriter}; +use bevy_ecs::schedule::SystemSet; +use bevy_ecs::system::Commands; +use bevy_ecs::{ + component::Component, entity::Entity, event::EventReader, query::With, + schedule::IntoSystemConfigs, system::Query, +}; +use thiserror::Error; + +use crate::client::Client; +use crate::packet::game::SendPacketEvent; + +#[derive(Error, Debug)] +pub enum MovePlayerError { + #[error("Player is not in world")] + PlayerNotInWorld(Backtrace), + #[error("{0}")] + Io(#[from] std::io::Error), +} + +impl From for MovePlayerError { + fn from(err: MoveEntityError) -> Self { + match err { + MoveEntityError::EntityDoesNotExist(backtrace) => { + MovePlayerError::PlayerNotInWorld(backtrace) + } + } + } +} + +pub struct MovementPlugin; + +impl Plugin for MovementPlugin { + fn build(&self, app: &mut App) { + app.add_event::() + .add_event::() + .add_event::() + .add_systems( + Update, + (handle_sprint, handle_walk, handle_knockback) + .chain() + .in_set(MoveEventsSet), + ) + .add_systems( + GameTick, + ( + (tick_controls, local_player_ai_step) + .chain() + .in_set(PhysicsSet) + .before(ai_step) + .before(azalea_physics::fluids::update_in_water_state_and_do_fluid_pushing), + send_sprinting_if_needed.after(azalea_entity::update_in_loaded_chunk), + send_player_input_packet, + send_position.after(PhysicsSet), + ) + .chain(), + ); + } +} + +#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)] +pub struct MoveEventsSet; + +impl Client { + /// Set whether we're jumping. This acts as if you held space in + /// vanilla. If you want to jump once, use the `jump` function. + /// + /// If you're making a realistic client, calling this function every tick is + /// recommended. + pub fn set_jumping(&mut self, jumping: bool) { + let mut ecs = self.ecs.lock(); + let mut jumping_mut = self.query::<&mut Jumping>(&mut ecs); + **jumping_mut = jumping; + } + + /// Returns whether the player will try to jump next tick. + pub fn jumping(&self) -> bool { + *self.component::() + } + + /// Sets the direction the client is looking. `y_rot` is yaw (looking to the + /// side), `x_rot` is pitch (looking up and down). You can get these + /// numbers from the vanilla f3 screen. + /// `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 LookDirection>(&mut ecs); + + (look_direction.y_rot, look_direction.x_rot) = (y_rot, x_rot); + } + + /// Returns the direction the client is looking. The first value is the y + /// rotation (ie. yaw, looking to the side) and the second value is the x + /// rotation (ie. pitch, looking up and down). + pub fn direction(&self) -> (f32, f32) { + let look_direction = self.component::(); + (look_direction.y_rot, look_direction.x_rot) + } +} + +/// A component that contains the look direction that was last sent over the +/// network. +#[derive(Debug, Component, Clone, Default)] +pub struct LastSentLookDirection { + pub x_rot: f32, + pub y_rot: f32, +} + +/// Component for entities that can move and sprint. Usually only in +/// [`LocalEntity`]s. +/// +/// [`LocalEntity`]: azalea_entity::LocalEntity +#[derive(Default, Component, Clone)] +pub struct PhysicsState { + /// Minecraft only sends a movement packet either after 20 ticks or if the + /// player moved enough. This is that tick counter. + pub position_remainder: u32, + pub was_sprinting: bool, + // Whether we're going to try to start sprinting this tick. Equivalent to + // holding down ctrl for a tick. + pub trying_to_sprint: bool, + + pub move_direction: WalkDirection, + pub forward_impulse: f32, + pub left_impulse: f32, +} + +#[allow(clippy::type_complexity)] +pub fn send_position( + mut query: Query< + ( + Entity, + &Position, + &LookDirection, + &mut PhysicsState, + &mut LastSentPosition, + &mut Physics, + &mut LastSentLookDirection, + ), + With, + >, + mut send_packet_events: EventWriter, +) { + for ( + entity, + position, + direction, + mut physics_state, + mut last_sent_position, + mut physics, + mut last_direction, + ) in query.iter_mut() + { + let packet = { + // TODO: the camera being able to be controlled by other entities isn't + // implemented yet if !self.is_controlled_camera() { return }; + + let x_delta = position.x - last_sent_position.x; + let y_delta = position.y - last_sent_position.y; + let z_delta = position.z - last_sent_position.z; + let y_rot_delta = (direction.y_rot - last_direction.y_rot) as f64; + let x_rot_delta = (direction.x_rot - last_direction.x_rot) as f64; + + physics_state.position_remainder += 1; + + // boolean sendingPosition = Mth.lengthSquared(xDelta, yDelta, zDelta) > + // Mth.square(2.0E-4D) || this.positionReminder >= 20; + let sending_position = ((x_delta.powi(2) + y_delta.powi(2) + z_delta.powi(2)) + > 2.0e-4f64.powi(2)) + || physics_state.position_remainder >= 20; + let sending_direction = y_rot_delta != 0.0 || x_rot_delta != 0.0; + + // if self.is_passenger() { + // TODO: posrot packet for being a passenger + // } + let packet = if sending_position && sending_direction { + Some( + ServerboundMovePlayerPosRot { + pos: **position, + look_direction: *direction, + on_ground: physics.on_ground(), + } + .into_variant(), + ) + } else if sending_position { + Some( + ServerboundMovePlayerPos { + pos: **position, + on_ground: physics.on_ground(), + } + .into_variant(), + ) + } else if sending_direction { + Some( + ServerboundMovePlayerRot { + look_direction: *direction, + on_ground: physics.on_ground(), + } + .into_variant(), + ) + } else if physics.last_on_ground() != physics.on_ground() { + Some( + ServerboundMovePlayerStatusOnly { + on_ground: physics.on_ground(), + } + .into_variant(), + ) + } else { + None + }; + + if sending_position { + **last_sent_position = **position; + physics_state.position_remainder = 0; + } + if sending_direction { + last_direction.y_rot = direction.y_rot; + last_direction.x_rot = direction.x_rot; + } + + let on_ground = physics.on_ground(); + physics.set_last_on_ground(on_ground); + // minecraft checks for autojump here, but also autojump is bad so + + packet + }; + + if let Some(packet) = packet { + send_packet_events.send(SendPacketEvent { + sent_by: entity, + packet, + }); + } + } +} + +#[derive(Debug, Default, Component, Clone, PartialEq, Eq)] +pub struct LastSentInput(pub ServerboundPlayerInput); +pub fn send_player_input_packet( + mut query: Query<(Entity, &PhysicsState, &Jumping, Option<&LastSentInput>)>, + mut send_packet_events: EventWriter, + mut commands: Commands, +) { + for (entity, physics_state, jumping, last_sent_input) in query.iter_mut() { + let dir = physics_state.move_direction; + type D = WalkDirection; + let input = ServerboundPlayerInput { + forward: matches!(dir, D::Forward | D::ForwardLeft | D::ForwardRight), + backward: matches!(dir, D::Backward | D::BackwardLeft | D::BackwardRight), + left: matches!(dir, D::Left | D::ForwardLeft | D::BackwardLeft), + right: matches!(dir, D::Right | D::ForwardRight | D::BackwardRight), + jump: **jumping, + // TODO: implement sneaking + shift: false, + sprint: physics_state.trying_to_sprint, + }; + + // if LastSentInput isn't present, we default to assuming we're not pressing any + // keys and insert it anyways every time it changes + let last_sent_input = last_sent_input.cloned().unwrap_or_default(); + + if input != last_sent_input.0 { + send_packet_events.send(SendPacketEvent { + sent_by: entity, + packet: input.clone().into_variant(), + }); + commands.entity(entity).insert(LastSentInput(input)); + } + } +} + +fn send_sprinting_if_needed( + mut query: Query<(Entity, &MinecraftEntityId, &Sprinting, &mut PhysicsState)>, + mut send_packet_events: EventWriter, +) { + for (entity, minecraft_entity_id, sprinting, mut physics_state) in query.iter_mut() { + let was_sprinting = physics_state.was_sprinting; + if **sprinting != was_sprinting { + let sprinting_action = if **sprinting { + azalea_protocol::packets::game::s_player_command::Action::StartSprinting + } else { + azalea_protocol::packets::game::s_player_command::Action::StopSprinting + }; + send_packet_events.send(SendPacketEvent::new( + entity, + ServerboundPlayerCommand { + id: *minecraft_entity_id, + action: sprinting_action, + data: 0, + }, + )); + physics_state.was_sprinting = **sprinting; + } + } +} + +/// Update the impulse from self.move_direction. The multiplier is used for +/// sneaking. +pub(crate) fn tick_controls(mut query: Query<&mut PhysicsState>) { + for mut physics_state in query.iter_mut() { + let multiplier: Option = None; + + let mut forward_impulse: f32 = 0.; + let mut left_impulse: f32 = 0.; + let move_direction = physics_state.move_direction; + match move_direction { + WalkDirection::Forward | WalkDirection::ForwardRight | WalkDirection::ForwardLeft => { + forward_impulse += 1.; + } + WalkDirection::Backward + | WalkDirection::BackwardRight + | WalkDirection::BackwardLeft => { + forward_impulse -= 1.; + } + _ => {} + }; + match move_direction { + WalkDirection::Right | WalkDirection::ForwardRight | WalkDirection::BackwardRight => { + left_impulse += 1.; + } + WalkDirection::Left | WalkDirection::ForwardLeft | WalkDirection::BackwardLeft => { + left_impulse -= 1.; + } + _ => {} + }; + physics_state.forward_impulse = forward_impulse; + physics_state.left_impulse = left_impulse; + + if let Some(multiplier) = multiplier { + physics_state.forward_impulse *= multiplier; + physics_state.left_impulse *= multiplier; + } + } +} + +/// Makes the bot do one physics tick. Note that this is already handled +/// automatically by the client. +pub fn local_player_ai_step( + mut query: Query< + (&PhysicsState, &mut Physics, &mut Sprinting, &mut Attributes), + With, + >, +) { + for (physics_state, mut physics, mut sprinting, mut attributes) in query.iter_mut() { + // server ai step + physics.x_acceleration = physics_state.left_impulse; + physics.z_acceleration = physics_state.forward_impulse; + + // TODO: food data and abilities + // let has_enough_food_to_sprint = self.food_data().food_level || + // self.abilities().may_fly; + let has_enough_food_to_sprint = true; + + // TODO: double tapping w to sprint i think + + let trying_to_sprint = physics_state.trying_to_sprint; + + if !**sprinting + && ( + // !self.is_in_water() + // || self.is_underwater() && + has_enough_impulse_to_start_sprinting(physics_state) + && has_enough_food_to_sprint + // && !self.using_item() + // && !self.has_effect(MobEffects.BLINDNESS) + && trying_to_sprint + ) + { + set_sprinting(true, &mut sprinting, &mut attributes); + } + } +} + +impl Client { + /// Start walking in the given direction. To sprint, use + /// [`Client::sprint`]. To stop walking, call walk with + /// `WalkDirection::None`. + /// + /// # Examples + /// + /// Walk for 1 second + /// ```rust,no_run + /// # use azalea_client::{Client, WalkDirection}; + /// # use std::time::Duration; + /// # async fn example(mut bot: Client) { + /// bot.walk(WalkDirection::Forward); + /// tokio::time::sleep(Duration::from_secs(1)).await; + /// bot.walk(WalkDirection::None); + /// # } + /// ``` + pub fn walk(&mut self, direction: WalkDirection) { + let mut ecs = self.ecs.lock(); + ecs.send_event(StartWalkEvent { + entity: self.entity, + direction, + }); + } + + /// Start sprinting in the given direction. To stop moving, call + /// [`Client::walk(WalkDirection::None)`] + /// + /// # Examples + /// + /// Sprint for 1 second + /// ```rust,no_run + /// # use azalea_client::{Client, WalkDirection, SprintDirection}; + /// # use std::time::Duration; + /// # async fn example(mut bot: Client) { + /// bot.sprint(SprintDirection::Forward); + /// tokio::time::sleep(Duration::from_secs(1)).await; + /// bot.walk(WalkDirection::None); + /// # } + /// ``` + pub fn sprint(&mut self, direction: SprintDirection) { + let mut ecs = self.ecs.lock(); + ecs.send_event(StartSprintEvent { + entity: self.entity, + direction, + }); + } +} + +/// An event sent when the client starts walking. This does not get sent for +/// non-local entities. +/// +/// To stop walking or sprinting, send this event with `WalkDirection::None`. +#[derive(Event, Debug)] +pub struct StartWalkEvent { + pub entity: Entity, + pub direction: WalkDirection, +} + +/// The system that makes the player start walking when they receive a +/// [`StartWalkEvent`]. +pub fn handle_walk( + mut events: EventReader, + mut query: Query<(&mut PhysicsState, &mut Sprinting, &mut Attributes)>, +) { + for event in events.read() { + if let Ok((mut physics_state, mut sprinting, mut attributes)) = query.get_mut(event.entity) + { + physics_state.move_direction = event.direction; + physics_state.trying_to_sprint = false; + set_sprinting(false, &mut sprinting, &mut attributes); + } + } +} + +/// An event sent when the client starts sprinting. This does not get sent for +/// non-local entities. +#[derive(Event)] +pub struct StartSprintEvent { + pub entity: Entity, + pub direction: SprintDirection, +} +/// The system that makes the player start sprinting when they receive a +/// [`StartSprintEvent`]. +pub fn handle_sprint( + mut query: Query<&mut PhysicsState>, + mut events: EventReader, +) { + for event in events.read() { + if let Ok(mut physics_state) = query.get_mut(event.entity) { + physics_state.move_direction = WalkDirection::from(event.direction); + physics_state.trying_to_sprint = true; + } + } +} + +/// Change whether we're sprinting by adding an attribute modifier to the +/// player. You should use the [`walk`] and [`sprint`] methods instead. +/// Returns if the operation was successful. +fn set_sprinting( + sprinting: bool, + currently_sprinting: &mut Sprinting, + attributes: &mut Attributes, +) -> bool { + **currently_sprinting = sprinting; + if sprinting { + attributes + .speed + .try_insert(azalea_entity::attributes::sprinting_modifier()) + .is_ok() + } else { + attributes + .speed + .remove(&azalea_entity::attributes::sprinting_modifier().id) + .is_none() + } +} + +// Whether the player is moving fast enough to be able to start sprinting. +fn has_enough_impulse_to_start_sprinting(physics_state: &PhysicsState) -> bool { + // if self.underwater() { + // self.has_forward_impulse() + // } else { + physics_state.forward_impulse > 0.8 + // } +} + +/// An event sent by the server that sets or adds to our velocity. Usually +/// `KnockbackKind::Set` is used for normal knockback and `KnockbackKind::Add` +/// is used for explosions, but some servers (notably Hypixel) use explosions +/// for knockback. +#[derive(Event)] +pub struct KnockbackEvent { + pub entity: Entity, + pub knockback: KnockbackType, +} + +pub enum KnockbackType { + Set(Vec3), + Add(Vec3), +} + +pub fn handle_knockback(mut query: Query<&mut Physics>, mut events: EventReader) { + for event in events.read() { + if let Ok(mut physics) = query.get_mut(event.entity) { + match event.knockback { + KnockbackType::Set(velocity) => { + physics.velocity = velocity; + } + KnockbackType::Add(velocity) => { + physics.velocity += velocity; + } + } + } + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum WalkDirection { + #[default] + None, + Forward, + Backward, + Left, + Right, + ForwardRight, + ForwardLeft, + BackwardRight, + BackwardLeft, +} + +/// The directions that we can sprint in. It's a subset of [`WalkDirection`]. +#[derive(Clone, Copy, Debug)] +pub enum SprintDirection { + Forward, + ForwardRight, + ForwardLeft, +} + +impl From for WalkDirection { + fn from(d: SprintDirection) -> Self { + match d { + SprintDirection::Forward => WalkDirection::Forward, + SprintDirection::ForwardRight => WalkDirection::ForwardRight, + SprintDirection::ForwardLeft => WalkDirection::ForwardLeft, + } + } +} diff --git a/azalea-client/src/plugins/packet/config/events.rs b/azalea-client/src/plugins/packet/config/events.rs new file mode 100644 index 00000000..6b647d74 --- /dev/null +++ b/azalea-client/src/plugins/packet/config/events.rs @@ -0,0 +1,90 @@ +use std::io::Cursor; + +use azalea_protocol::{ + packets::{ + config::{ClientboundConfigPacket, ServerboundConfigPacket}, + Packet, + }, + read::deserialize_packet, +}; +use bevy_ecs::prelude::*; +use tracing::{debug, error}; + +use crate::{raw_connection::RawConnection, InConfigState}; + +#[derive(Event, Debug, Clone)] +pub struct ReceiveConfigPacketEvent { + /// The client entity that received the packet. + pub entity: Entity, + /// The packet that was actually received. + pub packet: ClientboundConfigPacket, +} + +/// An event for sending a packet to the server while we're in the +/// `configuration` state. +#[derive(Event)] +pub struct SendConfigPacketEvent { + pub sent_by: Entity, + pub packet: ServerboundConfigPacket, +} +impl SendConfigPacketEvent { + pub fn new(sent_by: Entity, packet: impl Packet) -> Self { + let packet = packet.into_variant(); + Self { sent_by, packet } + } +} + +pub fn handle_send_packet_event( + mut send_packet_events: EventReader, + mut query: Query<(&mut RawConnection, Option<&InConfigState>)>, +) { + for event in send_packet_events.read() { + if let Ok((raw_conn, in_configuration_state)) = query.get_mut(event.sent_by) { + if in_configuration_state.is_none() { + error!( + "Tried to send a configuration packet {:?} while not in configuration state", + event.packet + ); + continue; + } + debug!("Sending packet: {:?}", event.packet); + if let Err(e) = raw_conn.write_packet(event.packet.clone()) { + error!("Failed to send packet: {e}"); + } + } + } +} + +pub fn send_packet_events( + query: Query<(Entity, &RawConnection), With>, + mut packet_events: ResMut>, +) { + // we manually clear and send the events at the beginning of each update + // since otherwise it'd cause issues with events in process_packet_events + // running twice + packet_events.clear(); + for (player_entity, raw_conn) in &query { + let packets_lock = raw_conn.incoming_packet_queue(); + let mut packets = packets_lock.lock(); + if !packets.is_empty() { + for raw_packet in packets.iter() { + let packet = match deserialize_packet::(&mut Cursor::new( + raw_packet, + )) { + Ok(packet) => packet, + Err(err) => { + error!("failed to read packet: {err:?}"); + debug!("packet bytes: {raw_packet:?}"); + continue; + } + }; + packet_events.send(ReceiveConfigPacketEvent { + entity: player_entity, + packet, + }); + } + // clear the packets right after we read them + packets.clear(); + } + } +} diff --git a/azalea-client/src/plugins/packet/config/mod.rs b/azalea-client/src/plugins/packet/config/mod.rs new file mode 100644 index 00000000..5cb19b9d --- /dev/null +++ b/azalea-client/src/plugins/packet/config/mod.rs @@ -0,0 +1,223 @@ +mod events; + +use azalea_protocol::packets::config::*; +use azalea_protocol::packets::ConnectionProtocol; +use bevy_ecs::prelude::*; +use bevy_ecs::system::SystemState; +pub use events::*; +use tracing::{debug, warn}; + +use super::as_system; +use crate::client::InConfigState; +use crate::disconnect::DisconnectEvent; +use crate::packet::game::KeepAliveEvent; +use crate::raw_connection::RawConnection; +use crate::{declare_packet_handlers, InstanceHolder}; + +pub fn process_packet_events(ecs: &mut World) { + let mut events_owned = Vec::new(); + let mut system_state: SystemState> = + SystemState::new(ecs); + let mut events = system_state.get_mut(ecs); + for ReceiveConfigPacketEvent { + entity: player_entity, + packet, + } in events.read() + { + // we do this so `ecs` isn't borrowed for the whole loop + events_owned.push((*player_entity, packet.clone())); + } + for (player_entity, packet) in events_owned { + let mut handler = ConfigPacketHandler { + player: player_entity, + ecs, + }; + + declare_packet_handlers!( + ClientboundConfigPacket, + packet, + handler, + [ + cookie_request, + custom_payload, + disconnect, + finish_configuration, + keep_alive, + ping, + reset_chat, + registry_data, + resource_pack_pop, + resource_pack_push, + store_cookie, + transfer, + update_enabled_features, + update_tags, + select_known_packs, + custom_report_details, + server_links, + ] + ); + } +} + +pub struct ConfigPacketHandler<'a> { + pub ecs: &'a mut World, + pub player: Entity, +} +impl ConfigPacketHandler<'_> { + pub fn registry_data(&mut self, p: ClientboundRegistryData) { + as_system::>(self.ecs, |mut query| { + let instance_holder = query.get_mut(self.player).unwrap(); + let mut instance = instance_holder.instance.write(); + + // add the new registry data + instance.registries.append(p.registry_id, p.entries); + }); + } + + pub fn custom_payload(&mut self, p: ClientboundCustomPayload) { + debug!("Got custom payload packet {p:?}"); + } + + pub fn disconnect(&mut self, p: ClientboundDisconnect) { + warn!("Got disconnect packet {p:?}"); + as_system::>(self.ecs, |mut events| { + events.send(DisconnectEvent { + entity: self.player, + reason: Some(p.reason), + }); + }); + } + + pub fn finish_configuration(&mut self, p: ClientboundFinishConfiguration) { + debug!("got FinishConfiguration packet: {p:?}"); + + as_system::<(Commands, Query<&mut RawConnection>)>( + self.ecs, + |(mut commands, mut query)| { + let mut raw_conn = query.get_mut(self.player).unwrap(); + + raw_conn + .write_packet(ServerboundFinishConfiguration) + .expect( + "we should be in the right state and encoding this packet shouldn't fail", + ); + raw_conn.set_state(ConnectionProtocol::Game); + + // these components are added now that we're going to be in the Game state + commands + .entity(self.player) + .remove::() + .insert(crate::JoinedClientBundle::default()); + }, + ); + } + + pub fn keep_alive(&mut self, p: ClientboundKeepAlive) { + debug!( + "Got keep alive packet (in configuration) {p:?} for {:?}", + self.player + ); + + as_system::<(Query<&RawConnection>, EventWriter<_>)>(self.ecs, |(query, mut events)| { + let raw_conn = query.get(self.player).unwrap(); + + events.send(KeepAliveEvent { + entity: self.player, + id: p.id, + }); + raw_conn + .write_packet(ServerboundKeepAlive { id: p.id }) + .unwrap(); + }); + } + + pub fn ping(&mut self, p: ClientboundPing) { + debug!("Got ping packet (in configuration) {p:?}"); + + as_system::>(self.ecs, |query| { + let raw_conn = query.get(self.player).unwrap(); + + raw_conn.write_packet(ServerboundPong { id: p.id }).unwrap(); + }); + } + + pub fn resource_pack_push(&mut self, p: ClientboundResourcePackPush) { + debug!("Got resource pack push packet {p:?}"); + + as_system::>(self.ecs, |query| { + let raw_conn = query.get(self.player).unwrap(); + + // always accept resource pack + raw_conn + .write_packet(ServerboundResourcePack { + id: p.id, + action: s_resource_pack::Action::Accepted, + }) + .unwrap(); + }); + } + + pub fn resource_pack_pop(&mut self, p: ClientboundResourcePackPop) { + debug!("Got resource pack pop packet {p:?}"); + } + + pub fn update_enabled_features(&mut self, p: ClientboundUpdateEnabledFeatures) { + debug!("Got update enabled features packet {p:?}"); + } + + pub fn update_tags(&mut self, _p: ClientboundUpdateTags) { + debug!("Got update tags packet"); + } + + pub fn cookie_request(&mut self, p: ClientboundCookieRequest) { + debug!("Got cookie request packet {p:?}"); + + as_system::>(self.ecs, |query| { + let raw_conn = query.get(self.player).unwrap(); + + raw_conn + .write_packet(ServerboundCookieResponse { + key: p.key, + // cookies aren't implemented + payload: None, + }) + .unwrap(); + }); + } + + pub fn reset_chat(&mut self, p: ClientboundResetChat) { + debug!("Got reset chat packet {p:?}"); + } + + pub fn store_cookie(&mut self, p: ClientboundStoreCookie) { + debug!("Got store cookie packet {p:?}"); + } + + pub fn transfer(&mut self, p: ClientboundTransfer) { + debug!("Got transfer packet {p:?}"); + } + + pub fn select_known_packs(&mut self, p: ClientboundSelectKnownPacks) { + debug!("Got select known packs packet {p:?}"); + + as_system::>(self.ecs, |query| { + let raw_conn = query.get(self.player).unwrap(); + + // resource pack management isn't implemented + raw_conn + .write_packet(ServerboundSelectKnownPacks { + known_packs: vec![], + }) + .unwrap(); + }); + } + + pub fn server_links(&mut self, p: ClientboundServerLinks) { + debug!("Got server links packet {p:?}"); + } + + pub fn custom_report_details(&mut self, p: ClientboundCustomReportDetails) { + debug!("Got custom report details packet {p:?}"); + } +} diff --git a/azalea-client/src/plugins/packet/game/events.rs b/azalea-client/src/plugins/packet/game/events.rs new file mode 100644 index 00000000..19f2a571 --- /dev/null +++ b/azalea-client/src/plugins/packet/game/events.rs @@ -0,0 +1,178 @@ +use std::{ + io::Cursor, + sync::{Arc, Weak}, +}; + +use azalea_chat::FormattedText; +use azalea_core::resource_location::ResourceLocation; +use azalea_entity::LocalEntity; +use azalea_protocol::{ + packets::{ + Packet, + game::{ClientboundGamePacket, ClientboundPlayerCombatKill, ServerboundGamePacket}, + }, + read::deserialize_packet, +}; +use azalea_world::Instance; +use bevy_ecs::prelude::*; +use parking_lot::RwLock; +use tracing::{debug, error}; +use uuid::Uuid; + +use crate::{PlayerInfo, raw_connection::RawConnection}; + +/// An event that's sent when we receive a packet. +/// ``` +/// # use azalea_client::packet::game::ReceivePacketEvent; +/// # use azalea_protocol::packets::game::ClientboundGamePacket; +/// # use bevy_ecs::event::EventReader; +/// +/// fn handle_packets(mut events: EventReader) { +/// for ReceivePacketEvent { +/// entity, +/// packet, +/// } in events.read() { +/// match packet.as_ref() { +/// ClientboundGamePacket::LevelParticles(p) => { +/// // ... +/// } +/// _ => {} +/// } +/// } +/// } +/// ``` +#[derive(Event, Debug, Clone)] +pub struct ReceivePacketEvent { + /// The client entity that received the packet. + pub entity: Entity, + /// The packet that was actually received. + pub packet: Arc, +} + +/// An event for sending a packet to the server while we're in the `game` state. +#[derive(Event)] +pub struct SendPacketEvent { + pub sent_by: Entity, + pub packet: ServerboundGamePacket, +} +impl SendPacketEvent { + pub fn new(sent_by: Entity, packet: impl Packet) -> Self { + let packet = packet.into_variant(); + Self { sent_by, packet } + } +} + +pub fn handle_outgoing_packets( + mut send_packet_events: EventReader, + mut query: Query<&mut RawConnection>, +) { + for event in send_packet_events.read() { + if let Ok(raw_connection) = query.get_mut(event.sent_by) { + // debug!("Sending packet: {:?}", event.packet); + if let Err(e) = raw_connection.write_packet(event.packet.clone()) { + error!("Failed to send packet: {e}"); + } + } + } +} + +pub fn send_receivepacketevent( + query: Query<(Entity, &RawConnection), With>, + mut packet_events: ResMut>, +) { + // we manually clear and send the events at the beginning of each update + // since otherwise it'd cause issues with events in process_packet_events + // running twice + packet_events.clear(); + for (player_entity, raw_connection) in &query { + let packets_lock = raw_connection.incoming_packet_queue(); + let mut packets = packets_lock.lock(); + if !packets.is_empty() { + for raw_packet in packets.iter() { + let packet = + match deserialize_packet::(&mut Cursor::new(raw_packet)) + { + Ok(packet) => packet, + Err(err) => { + error!("failed to read packet: {err:?}"); + debug!("packet bytes: {raw_packet:?}"); + continue; + } + }; + packet_events.send(ReceivePacketEvent { + entity: player_entity, + packet: Arc::new(packet), + }); + } + // clear the packets right after we read them + packets.clear(); + } + } +} + +/// A player joined the game (or more specifically, was added to the tab +/// list of a local player). +#[derive(Event, Debug, Clone)] +pub struct AddPlayerEvent { + /// The local player entity that received this event. + pub entity: Entity, + pub info: PlayerInfo, +} +/// A player left the game (or maybe is still in the game and was just +/// removed from the tab list of a local player). +#[derive(Event, Debug, Clone)] +pub struct RemovePlayerEvent { + /// The local player entity that received this event. + pub entity: Entity, + pub info: PlayerInfo, +} +/// A player was updated in the tab list of a local player (gamemode, display +/// name, or latency changed). +#[derive(Event, Debug, Clone)] +pub struct UpdatePlayerEvent { + /// The local player entity that received this event. + pub entity: Entity, + pub info: PlayerInfo, +} + +/// Event for when an entity dies. dies. If it's a local player and there's a +/// reason in the death screen, the [`ClientboundPlayerCombatKill`] will +/// be included. +#[derive(Event, Debug, Clone)] +pub struct DeathEvent { + pub entity: Entity, + pub packet: Option, +} + +/// A KeepAlive packet is sent from the server to verify that the client is +/// still connected. +#[derive(Event, Debug, Clone)] +pub struct KeepAliveEvent { + pub entity: Entity, + /// The ID of the keepalive. This is an arbitrary number, but vanilla + /// servers use the time to generate this. + pub id: u64, +} + +#[derive(Event, Debug, Clone)] +pub struct ResourcePackEvent { + pub entity: Entity, + /// The random ID for this request to download the resource pack. The packet + /// for replying to a resource pack push must contain the same ID. + pub id: Uuid, + pub url: String, + pub hash: String, + pub required: bool, + pub prompt: Option, +} + +/// An instance (aka world, dimension) was loaded by a client. +/// +/// Since the instance is given to you as a weak reference, it won't be able to +/// be `upgrade`d if all local players leave it. +#[derive(Event, Debug, Clone)] +pub struct InstanceLoadedEvent { + pub entity: Entity, + pub name: ResourceLocation, + pub instance: Weak>, +} diff --git a/azalea-client/src/plugins/packet/game/mod.rs b/azalea-client/src/plugins/packet/game/mod.rs new file mode 100644 index 00000000..98f76d13 --- /dev/null +++ b/azalea-client/src/plugins/packet/game/mod.rs @@ -0,0 +1,1583 @@ +mod events; + +use std::{collections::HashSet, ops::Add, sync::Arc}; + +use azalea_core::{ + game_type::GameMode, + math, + position::{ChunkPos, Vec3}, +}; +use azalea_entity::{ + Dead, EntityBundle, EntityKind, LastSentPosition, LoadedBy, LocalEntity, LookDirection, + Physics, Position, RelativeEntityUpdate, + indexing::{EntityIdIndex, EntityUuidIndex}, + metadata::{Health, apply_metadata}, +}; +use azalea_protocol::packets::game::*; +use azalea_world::{InstanceContainer, InstanceName, MinecraftEntityId, PartialInstance}; +use bevy_ecs::{prelude::*, system::SystemState}; +pub use events::*; +use tracing::{debug, error, trace, warn}; + +use crate::{ + ClientInformation, PlayerInfo, + chat::{ChatPacket, ChatReceivedEvent}, + chunks, declare_packet_handlers, + disconnect::DisconnectEvent, + inventory::{ + ClientSideCloseContainerEvent, Inventory, MenuOpenedEvent, SetContainerContentEvent, + }, + local_player::{ + GameProfileComponent, Hunger, InstanceHolder, LocalGameMode, PlayerAbilities, TabList, + }, + movement::{KnockbackEvent, KnockbackType}, + packet::as_system, +}; + +pub fn process_packet_events(ecs: &mut World) { + let mut events_owned = Vec::<(Entity, Arc)>::new(); + + { + let mut system_state = SystemState::>::new(ecs); + let mut events = system_state.get_mut(ecs); + for ReceivePacketEvent { + entity: player_entity, + packet, + } in events.read() + { + // we do this so `ecs` isn't borrowed for the whole loop + events_owned.push((*player_entity, packet.clone())); + } + } + + for (player_entity, packet) in events_owned { + let mut handler = GamePacketHandler { + player: player_entity, + ecs, + }; + + declare_packet_handlers!( + ClientboundGamePacket, + packet.as_ref(), + handler, + [ + login, + set_chunk_cache_radius, + chunk_batch_start, + chunk_batch_finished, + custom_payload, + change_difficulty, + commands, + player_abilities, + set_cursor_item, + update_tags, + disconnect, + update_recipes, + entity_event, + player_position, + player_info_update, + player_info_remove, + set_chunk_cache_center, + chunks_biomes, + light_update, + level_chunk_with_light, + add_entity, + set_entity_data, + update_attributes, + set_entity_motion, + set_entity_link, + initialize_border, + set_time, + set_default_spawn_position, + set_health, + set_experience, + teleport_entity, + update_advancements, + rotate_head, + move_entity_pos, + move_entity_pos_rot, + move_entity_rot, + keep_alive, + remove_entities, + player_chat, + system_chat, + disguised_chat, + sound, + level_event, + block_update, + animate, + section_blocks_update, + game_event, + level_particles, + server_data, + set_equipment, + update_mob_effect, + add_experience_orb, + award_stats, + block_changed_ack, + block_destruction, + block_entity_data, + block_event, + boss_event, + command_suggestions, + container_set_content, + container_set_data, + container_set_slot, + container_close, + cooldown, + custom_chat_completions, + delete_chat, + explode, + forget_level_chunk, + horse_screen_open, + map_item_data, + merchant_offers, + move_vehicle, + open_book, + open_screen, + open_sign_editor, + ping, + place_ghost_recipe, + player_combat_end, + player_combat_enter, + player_combat_kill, + player_look_at, + remove_mob_effect, + resource_pack_push, + resource_pack_pop, + respawn, + start_configuration, + entity_position_sync, + select_advancements_tab, + set_action_bar_text, + set_border_center, + set_border_lerp_size, + set_border_size, + set_border_warning_delay, + set_border_warning_distance, + set_camera, + set_display_objective, + set_objective, + set_passengers, + set_player_team, + set_score, + set_simulation_distance, + set_subtitle_text, + set_title_text, + set_titles_animation, + clear_titles, + sound_entity, + stop_sound, + tab_list, + tag_query, + take_item_entity, + bundle_delimiter, + damage_event, + hurt_animation, + ticking_state, + ticking_step, + reset_score, + cookie_request, + debug_sample, + pong_response, + store_cookie, + transfer, + move_minecart_along_track, + set_held_slot, + set_player_inventory, + projectile_power, + custom_report_details, + server_links, + player_rotation, + recipe_book_add, + recipe_book_remove, + recipe_book_settings, + ] + ); + } +} + +pub struct GamePacketHandler<'a> { + pub ecs: &'a mut World, + pub player: Entity, +} +impl GamePacketHandler<'_> { + pub fn login(&mut self, p: &ClientboundLogin) { + debug!("Got login packet"); + + as_system::<( + Commands, + Query<( + &GameProfileComponent, + &ClientInformation, + Option<&mut InstanceName>, + Option<&mut LoadedBy>, + &mut EntityIdIndex, + &mut InstanceHolder, + )>, + EventWriter, + ResMut, + ResMut, + EventWriter, + )>( + self.ecs, + |( + mut commands, + mut query, + mut instance_loaded_events, + mut instance_container, + mut entity_uuid_index, + mut send_packet_events, + )| { + let ( + game_profile, + client_information, + instance_name, + loaded_by, + mut entity_id_index, + mut instance_holder, + ) = query.get_mut(self.player).unwrap(); + + let new_instance_name = p.common.dimension.clone(); + + if let Some(mut instance_name) = instance_name { + *instance_name = instance_name.clone(); + } else { + commands + .entity(self.player) + .insert(InstanceName(new_instance_name.clone())); + } + + let Some((_dimension_type, dimension_data)) = p + .common + .dimension_type(&instance_holder.instance.read().registries) + else { + return; + }; + + // add this world to the instance_container (or don't if it's already + // there) + let weak_instance = instance_container.insert( + new_instance_name.clone(), + dimension_data.height, + dimension_data.min_y, + &instance_holder.instance.read().registries, + ); + instance_loaded_events.send(InstanceLoadedEvent { + entity: self.player, + name: new_instance_name.clone(), + instance: Arc::downgrade(&weak_instance), + }); + + // set the partial_world to an empty world + // (when we add chunks or entities those will be in the + // instance_container) + + *instance_holder.partial_instance.write() = PartialInstance::new( + azalea_world::chunk_storage::calculate_chunk_storage_range( + client_information.view_distance.into(), + ), + // this argument makes it so other clients don't update this player entity + // in a shared instance + Some(self.player), + ); + { + let map = instance_holder.instance.read().registries.map.clone(); + let new_registries = &mut weak_instance.write().registries; + // add the registries from this instance to the weak instance + for (registry_name, registry) in map { + new_registries.map.insert(registry_name, registry); + } + } + instance_holder.instance = weak_instance; + + let entity_bundle = EntityBundle::new( + game_profile.uuid, + Vec3::default(), + azalea_registry::EntityKind::Player, + new_instance_name, + ); + let entity_id = p.player_id; + // insert our components into the ecs :) + commands.entity(self.player).insert(( + entity_id, + LocalGameMode { + current: p.common.game_type, + previous: p.common.previous_game_type.into(), + }, + entity_bundle, + )); + + azalea_entity::indexing::add_entity_to_indexes( + entity_id, + self.player, + Some(game_profile.uuid), + &mut entity_id_index, + &mut entity_uuid_index, + &mut instance_holder.instance.write(), + ); + + // update or insert loaded_by + if let Some(mut loaded_by) = loaded_by { + loaded_by.insert(self.player); + } else { + commands + .entity(self.player) + .insert(LoadedBy(HashSet::from_iter(vec![self.player]))); + } + + // send the client information that we have set + debug!( + "Sending client information because login: {:?}", + client_information + ); + send_packet_events.send(SendPacketEvent::new(self.player, + azalea_protocol::packets::game::s_client_information::ServerboundClientInformation { information: client_information.clone() }, + )); + }, + ); + } + + pub fn set_chunk_cache_radius(&mut self, p: &ClientboundSetChunkCacheRadius) { + debug!("Got set chunk cache radius packet {p:?}"); + } + + pub fn chunk_batch_start(&mut self, _p: &ClientboundChunkBatchStart) { + // the packet is empty, it's just a marker to tell us when the batch starts and + // ends + debug!("Got chunk batch start"); + + as_system::>(self.ecs, |mut events| { + events.send(chunks::ChunkBatchStartEvent { + entity: self.player, + }); + }); + } + + pub fn chunk_batch_finished(&mut self, p: &ClientboundChunkBatchFinished) { + debug!("Got chunk batch finished {p:?}"); + + as_system::>(self.ecs, |mut events| { + events.send(chunks::ChunkBatchFinishedEvent { + entity: self.player, + batch_size: p.batch_size, + }); + }); + } + + pub fn custom_payload(&mut self, p: &ClientboundCustomPayload) { + debug!("Got custom payload packet {p:?}"); + } + + pub fn change_difficulty(&mut self, p: &ClientboundChangeDifficulty) { + debug!("Got difficulty packet {p:?}"); + } + + pub fn commands(&mut self, _p: &ClientboundCommands) { + debug!("Got declare commands packet"); + } + + pub fn player_abilities(&mut self, p: &ClientboundPlayerAbilities) { + debug!("Got player abilities packet {p:?}"); + + as_system::>(self.ecs, |mut query| { + let mut player_abilities = query.get_mut(self.player).unwrap(); + + *player_abilities = PlayerAbilities::from(p); + }); + } + + pub fn set_cursor_item(&mut self, p: &ClientboundSetCursorItem) { + debug!("Got set cursor item packet {p:?}"); + } + + pub fn update_tags(&mut self, _p: &ClientboundUpdateTags) { + debug!("Got update tags packet"); + } + + pub fn disconnect(&mut self, p: &ClientboundDisconnect) { + warn!("Got disconnect packet {p:?}"); + + as_system::>(self.ecs, |mut events| { + events.send(DisconnectEvent { + entity: self.player, + reason: Some(p.reason.clone()), + }); + }); + } + + pub fn update_recipes(&mut self, _p: &ClientboundUpdateRecipes) { + debug!("Got update recipes packet"); + } + + pub fn entity_event(&mut self, _p: &ClientboundEntityEvent) { + // debug!("Got entity event packet {p:?}"); + } + + pub fn player_position(&mut self, p: &ClientboundPlayerPosition) { + debug!("Got player position packet {p:?}"); + + as_system::<( + Query<( + &mut Physics, + &mut LookDirection, + &mut Position, + &mut LastSentPosition, + )>, + EventWriter, + )>(self.ecs, |(mut query, mut send_packet_events)| { + let Ok((mut physics, mut direction, mut position, mut last_sent_position)) = + query.get_mut(self.player) + else { + return; + }; + + **last_sent_position = **position; + + fn apply_change>(base: T, condition: bool, change: T) -> T { + if condition { base + change } else { change } + } + + let new_x = apply_change(position.x, p.relative.x, p.change.pos.x); + let new_y = apply_change(position.y, p.relative.y, p.change.pos.y); + let new_z = apply_change(position.z, p.relative.z, p.change.pos.z); + + let new_y_rot = apply_change( + direction.y_rot, + p.relative.y_rot, + p.change.look_direction.y_rot, + ); + let new_x_rot = apply_change( + direction.x_rot, + p.relative.x_rot, + p.change.look_direction.x_rot, + ); + + let mut new_delta_from_rotations = physics.velocity; + if p.relative.rotate_delta { + let y_rot_delta = direction.y_rot - new_y_rot; + let x_rot_delta = direction.x_rot - new_x_rot; + new_delta_from_rotations = new_delta_from_rotations + .x_rot(math::to_radians(x_rot_delta as f64) as f32) + .y_rot(math::to_radians(y_rot_delta as f64) as f32); + } + + let new_delta = Vec3::new( + apply_change( + new_delta_from_rotations.x, + p.relative.delta_x, + p.change.delta.x, + ), + apply_change( + new_delta_from_rotations.y, + p.relative.delta_y, + p.change.delta.y, + ), + apply_change( + new_delta_from_rotations.z, + p.relative.delta_z, + p.change.delta.z, + ), + ); + + // apply the updates + + physics.velocity = new_delta; + + (direction.y_rot, direction.x_rot) = (new_y_rot, new_x_rot); + + let new_pos = Vec3::new(new_x, new_y, new_z); + if new_pos != **position { + **position = new_pos; + } + + // old_pos is set to the current position when we're teleported + physics.set_old_pos(&position); + + // send the relevant packets + + send_packet_events.send(SendPacketEvent::new( + self.player, + ServerboundAcceptTeleportation { id: p.id }, + )); + send_packet_events.send(SendPacketEvent::new( + self.player, + ServerboundMovePlayerPosRot { + pos: new_pos, + look_direction: LookDirection::new(new_y_rot, new_x_rot), + // this is always false + on_ground: false, + }, + )); + }); + } + + pub fn player_info_update(&mut self, p: &ClientboundPlayerInfoUpdate) { + debug!("Got player info packet {p:?}"); + + as_system::<( + Query<&mut TabList>, + EventWriter, + EventWriter, + ResMut, + )>( + self.ecs, + |( + mut query, + mut add_player_events, + mut update_player_events, + mut tab_list_resource, + )| { + let mut tab_list = query.get_mut(self.player).unwrap(); + + for updated_info in &p.entries { + // add the new player maybe + if p.actions.add_player { + let info = PlayerInfo { + profile: updated_info.profile.clone(), + uuid: updated_info.profile.uuid, + gamemode: updated_info.game_mode, + latency: updated_info.latency, + display_name: updated_info.display_name.clone(), + }; + tab_list.insert(updated_info.profile.uuid, info.clone()); + add_player_events.send(AddPlayerEvent { + entity: self.player, + info: info.clone(), + }); + } else if let Some(info) = tab_list.get_mut(&updated_info.profile.uuid) { + // `else if` because the block for add_player above + // already sets all the fields + if p.actions.update_game_mode { + info.gamemode = updated_info.game_mode; + } + if p.actions.update_latency { + info.latency = updated_info.latency; + } + if p.actions.update_display_name { + info.display_name.clone_from(&updated_info.display_name); + } + update_player_events.send(UpdatePlayerEvent { + entity: self.player, + info: info.clone(), + }); + } else { + let uuid = updated_info.profile.uuid; + debug!("Ignoring PlayerInfoUpdate for unknown player {uuid}"); + } + } + + *tab_list_resource = tab_list.clone(); + }, + ); + } + + pub fn player_info_remove(&mut self, p: &ClientboundPlayerInfoRemove) { + debug!("Got chunk cache center packet {p:?}"); + + as_system::<( + Query<&mut TabList>, + EventWriter, + ResMut, + )>( + self.ecs, + |(mut query, mut remove_player_events, mut tab_list_resource)| { + let mut tab_list = query.get_mut(self.player).unwrap(); + + for uuid in &p.profile_ids { + if let Some(info) = tab_list.remove(uuid) { + remove_player_events.send(RemovePlayerEvent { + entity: self.player, + info, + }); + } + tab_list_resource.remove(uuid); + } + }, + ); + } + + pub fn set_chunk_cache_center(&mut self, p: &ClientboundSetChunkCacheCenter) { + debug!("Got chunk cache center packet {p:?}"); + + as_system::>(self.ecs, |mut query| { + let instance_holder = query.get_mut(self.player).unwrap(); + let mut partial_world = instance_holder.partial_instance.write(); + + partial_world + .chunks + .update_view_center(ChunkPos::new(p.x, p.z)); + }); + } + + pub fn chunks_biomes(&mut self, _p: &ClientboundChunksBiomes) {} + + pub fn light_update(&mut self, _p: &ClientboundLightUpdate) { + // debug!("Got light update packet {p:?}"); + } + + pub fn level_chunk_with_light(&mut self, p: &ClientboundLevelChunkWithLight) { + debug!("Got chunk with light packet {} {}", p.x, p.z); + + as_system::>(self.ecs, |mut events| { + events.send(chunks::ReceiveChunkEvent { + entity: self.player, + packet: p.clone(), + }); + }); + } + + pub fn add_entity(&mut self, p: &ClientboundAddEntity) { + debug!("Got add entity packet {p:?}"); + + as_system::<( + Commands, + Query<(&mut EntityIdIndex, Option<&InstanceName>, Option<&TabList>)>, + Query<&mut LoadedBy>, + Query, + Res, + ResMut, + )>( + self.ecs, + |( + mut commands, + mut query, + mut loaded_by_query, + entity_query, + instance_container, + mut entity_uuid_index, + )| { + let (mut entity_id_index, instance_name, tab_list) = + query.get_mut(self.player).unwrap(); + + let entity_id = p.id; + + let Some(instance_name) = instance_name else { + warn!("got add player packet but we haven't gotten a login packet yet"); + return; + }; + + // check if the entity already exists, and if it does then only add to LoadedBy + let instance = instance_container.get(instance_name).unwrap(); + if let Some(&ecs_entity) = instance.read().entity_by_id.get(&entity_id) { + // entity already exists + let Ok(mut loaded_by) = loaded_by_query.get_mut(ecs_entity) else { + // LoadedBy for this entity isn't in the ecs! figure out what went wrong + // and print an error + + let entity_in_ecs = entity_query.get(ecs_entity).is_ok(); + + if entity_in_ecs { + error!( + "LoadedBy for entity {entity_id:?} ({ecs_entity:?}) isn't in the ecs, but the entity is in entity_by_id" + ); + } else { + error!( + "Entity {entity_id:?} ({ecs_entity:?}) isn't in the ecs, but the entity is in entity_by_id" + ); + } + return; + }; + loaded_by.insert(self.player); + + // per-client id index + entity_id_index.insert(entity_id, ecs_entity); + + debug!("added to LoadedBy of entity {ecs_entity:?} with id {entity_id:?}"); + return; + }; + + // entity doesn't exist in the global index! + + let bundle = p.as_entity_bundle((**instance_name).clone()); + let mut spawned = + commands.spawn((entity_id, LoadedBy(HashSet::from([self.player])), bundle)); + let ecs_entity: Entity = spawned.id(); + debug!("spawned entity {ecs_entity:?} with id {entity_id:?}"); + + azalea_entity::indexing::add_entity_to_indexes( + entity_id, + ecs_entity, + Some(p.uuid), + &mut entity_id_index, + &mut entity_uuid_index, + &mut instance.write(), + ); + + // add the GameProfileComponent if the uuid is in the tab list + if let Some(tab_list) = tab_list { + // (technically this makes it possible for non-player entities to have + // GameProfileComponents but the server would have to be doing something + // really weird) + if let Some(player_info) = tab_list.get(&p.uuid) { + spawned.insert(GameProfileComponent(player_info.profile.clone())); + } + } + + // the bundle doesn't include the default entity metadata so we add that + // separately + p.apply_metadata(&mut spawned); + }, + ); + } + + pub fn set_entity_data(&mut self, p: &ClientboundSetEntityData) { + as_system::<( + Commands, + Query<(&EntityIdIndex, &InstanceHolder)>, + // this is a separate query since it's applied on the entity id that's being updated + // instead of the player that received the packet + Query<&EntityKind>, + )>(self.ecs, |(mut commands, query, entity_kind_query)| { + let (entity_id_index, instance_holder) = query.get(self.player).unwrap(); + + let entity = entity_id_index.get(p.id); + + let Some(entity) = entity else { + // some servers like hypixel trigger this a lot :( + debug!( + "Server sent an entity data packet for an entity id ({}) that we don't know about", + p.id + ); + return; + }; + + let entity_kind = *entity_kind_query + .get(entity) + .expect("EntityKind component should always be present for entities"); + + debug!("Got set entity data packet {p:?} for entity of kind {entity_kind:?}"); + + let packed_items = p.packed_items.clone().to_vec(); + + // we use RelativeEntityUpdate because it makes sure changes aren't made + // multiple times + commands.entity(entity).queue(RelativeEntityUpdate { + partial_world: instance_holder.partial_instance.clone(), + update: Box::new(move |entity| { + let entity_id = entity.id(); + entity.world_scope(|world| { + let mut commands_system_state = SystemState::::new(world); + let mut commands = commands_system_state.get_mut(world); + let mut entity_commands = commands.entity(entity_id); + if let Err(e) = + apply_metadata(&mut entity_commands, *entity_kind, packed_items) + { + warn!("{e}"); + } + commands_system_state.apply(world); + }); + }), + }); + }); + } + + pub fn update_attributes(&mut self, _p: &ClientboundUpdateAttributes) { + // debug!("Got update attributes packet {p:?}"); + } + + pub fn set_entity_motion(&mut self, p: &ClientboundSetEntityMotion) { + // vanilla servers use this packet for knockback, but note that the Explode + // packet is also sometimes used by servers for knockback + + as_system::<(Commands, Query<(&EntityIdIndex, &InstanceHolder)>)>( + self.ecs, + |(mut commands, query)| { + let (entity_id_index, instance_holder) = query.get(self.player).unwrap(); + + let Some(entity) = entity_id_index.get(p.id) else { + // note that this log (and some other ones like the one in RemoveEntities) + // sometimes happens when killing mobs. it seems to be a vanilla bug, which is + // why it's a debug log instead of a warning + debug!( + "Got set entity motion packet for unknown entity id {}", + p.id + ); + return; + }; + + // this is to make sure the same entity velocity update doesn't get sent + // multiple times when in swarms + + let knockback = KnockbackType::Set(Vec3 { + x: p.delta.xa as f64 / 8000., + y: p.delta.ya as f64 / 8000., + z: p.delta.za as f64 / 8000., + }); + + commands.entity(entity).queue(RelativeEntityUpdate { + partial_world: instance_holder.partial_instance.clone(), + update: Box::new(move |entity_mut| { + entity_mut.world_scope(|world| { + world.send_event(KnockbackEvent { entity, knockback }) + }); + }), + }); + }, + ); + } + + pub fn set_entity_link(&mut self, p: &ClientboundSetEntityLink) { + debug!("Got set entity link packet {p:?}"); + } + + pub fn initialize_border(&mut self, p: &ClientboundInitializeBorder) { + debug!("Got initialize border packet {p:?}"); + } + + pub fn set_time(&mut self, _p: &ClientboundSetTime) { + // debug!("Got set time packet {p:?}"); + } + + pub fn set_default_spawn_position(&mut self, p: &ClientboundSetDefaultSpawnPosition) { + debug!("Got set default spawn position packet {p:?}"); + } + + pub fn set_health(&mut self, p: &ClientboundSetHealth) { + debug!("Got set health packet {p:?}"); + + as_system::>(self.ecs, |mut query| { + let (mut health, mut hunger) = query.get_mut(self.player).unwrap(); + + **health = p.health; + (hunger.food, hunger.saturation) = (p.food, p.saturation); + + // the `Dead` component is added by the `update_dead` system + // in azalea-world and then the `dead_event` system fires + // the Death event. + }); + } + + pub fn set_experience(&mut self, p: &ClientboundSetExperience) { + debug!("Got set experience packet {p:?}"); + } + + pub fn teleport_entity(&mut self, p: &ClientboundTeleportEntity) { + as_system::<(Commands, Query<(&EntityIdIndex, &InstanceHolder)>)>( + self.ecs, + |(mut commands, mut query)| { + let (entity_id_index, instance_holder) = query.get_mut(self.player).unwrap(); + + let Some(entity) = entity_id_index.get(p.id) else { + warn!("Got teleport entity packet for unknown entity id {}", p.id); + return; + }; + + let new_pos = p.change.pos; + let new_look_direction = LookDirection { + x_rot: (p.change.look_direction.x_rot as i32 * 360) as f32 / 256., + y_rot: (p.change.look_direction.y_rot as i32 * 360) as f32 / 256., + }; + commands.entity(entity).queue(RelativeEntityUpdate { + partial_world: instance_holder.partial_instance.clone(), + update: Box::new(move |entity| { + let mut position = entity.get_mut::().unwrap(); + if new_pos != **position { + **position = new_pos; + } + let position = *position; + let mut look_direction = entity.get_mut::().unwrap(); + if new_look_direction != *look_direction { + *look_direction = new_look_direction; + } + // old_pos is set to the current position when we're teleported + let mut physics = entity.get_mut::().unwrap(); + physics.set_old_pos(&position); + }), + }); + }, + ); + } + + pub fn update_advancements(&mut self, p: &ClientboundUpdateAdvancements) { + debug!("Got update advancements packet {p:?}"); + } + + pub fn rotate_head(&mut self, _p: &ClientboundRotateHead) {} + + pub fn move_entity_pos(&mut self, p: &ClientboundMoveEntityPos) { + as_system::<(Commands, Query<(&EntityIdIndex, &InstanceHolder)>)>( + self.ecs, + |(mut commands, mut query)| { + let (entity_id_index, instance_holder) = query.get_mut(self.player).unwrap(); + + debug!("Got move entity pos packet {p:?}"); + + let Some(entity) = entity_id_index.get(p.entity_id) else { + debug!( + "Got move entity pos packet for unknown entity id {}", + p.entity_id + ); + return; + }; + + let new_delta = p.delta.clone(); + let new_on_ground = p.on_ground; + commands.entity(entity).queue(RelativeEntityUpdate { + partial_world: instance_holder.partial_instance.clone(), + update: Box::new(move |entity_mut| { + let mut physics = entity_mut.get_mut::().unwrap(); + let new_pos = physics.vec_delta_codec.decode( + new_delta.xa as i64, + new_delta.ya as i64, + new_delta.za as i64, + ); + physics.vec_delta_codec.set_base(new_pos); + physics.set_on_ground(new_on_ground); + + let mut position = entity_mut.get_mut::().unwrap(); + if new_pos != **position { + **position = new_pos; + } + }), + }); + }, + ); + } + + pub fn move_entity_pos_rot(&mut self, p: &ClientboundMoveEntityPosRot) { + as_system::<(Commands, Query<(&EntityIdIndex, &InstanceHolder)>)>( + self.ecs, + |(mut commands, mut query)| { + let (entity_id_index, instance_holder) = query.get_mut(self.player).unwrap(); + + debug!("Got move entity pos rot packet {p:?}"); + + let entity = entity_id_index.get(p.entity_id); + + let Some(entity) = entity else { + // often triggered by hypixel :( + debug!( + "Got move entity pos rot packet for unknown entity id {}", + p.entity_id + ); + return; + }; + + let new_delta = p.delta.clone(); + let new_look_direction = LookDirection { + x_rot: (p.x_rot as i32 * 360) as f32 / 256., + y_rot: (p.y_rot as i32 * 360) as f32 / 256., + }; + + let new_on_ground = p.on_ground; + + commands.entity(entity).queue(RelativeEntityUpdate { + partial_world: instance_holder.partial_instance.clone(), + update: Box::new(move |entity_mut| { + let mut physics = entity_mut.get_mut::().unwrap(); + let new_pos = physics.vec_delta_codec.decode( + new_delta.xa as i64, + new_delta.ya as i64, + new_delta.za as i64, + ); + physics.vec_delta_codec.set_base(new_pos); + physics.set_on_ground(new_on_ground); + + let mut position = entity_mut.get_mut::().unwrap(); + if new_pos != **position { + **position = new_pos; + } + + let mut look_direction = entity_mut.get_mut::().unwrap(); + if new_look_direction != *look_direction { + *look_direction = new_look_direction; + } + }), + }); + }, + ); + } + + pub fn move_entity_rot(&mut self, p: &ClientboundMoveEntityRot) { + as_system::<(Commands, Query<(&EntityIdIndex, &InstanceHolder)>)>( + self.ecs, + |(mut commands, mut query)| { + let (entity_id_index, instance_holder) = query.get_mut(self.player).unwrap(); + + let entity = entity_id_index.get(p.entity_id); + if let Some(entity) = entity { + let new_look_direction = LookDirection { + x_rot: (p.x_rot as i32 * 360) as f32 / 256., + y_rot: (p.y_rot as i32 * 360) as f32 / 256., + }; + let new_on_ground = p.on_ground; + + commands.entity(entity).queue(RelativeEntityUpdate { + partial_world: instance_holder.partial_instance.clone(), + update: Box::new(move |entity_mut| { + let mut physics = entity_mut.get_mut::().unwrap(); + physics.set_on_ground(new_on_ground); + + let mut look_direction = entity_mut.get_mut::().unwrap(); + if new_look_direction != *look_direction { + *look_direction = new_look_direction; + } + }), + }); + } else { + warn!( + "Got move entity rot packet for unknown entity id {}", + p.entity_id + ); + } + }, + ); + } + pub fn keep_alive(&mut self, p: &ClientboundKeepAlive) { + debug!("Got keep alive packet {p:?} for {:?}", self.player); + + as_system::<(EventWriter, EventWriter)>( + self.ecs, + |(mut keepalive_events, mut send_packet_events)| { + keepalive_events.send(KeepAliveEvent { + entity: self.player, + id: p.id, + }); + send_packet_events.send(SendPacketEvent::new( + self.player, + ServerboundKeepAlive { id: p.id }, + )); + }, + ); + } + + pub fn remove_entities(&mut self, p: &ClientboundRemoveEntities) { + debug!("Got remove entities packet {p:?}"); + + as_system::<(Query<&mut EntityIdIndex>, Query<&mut LoadedBy>)>( + self.ecs, + |(mut query, mut entity_query)| { + let Ok(mut entity_id_index) = query.get_mut(self.player) else { + warn!("our local player doesn't have EntityIdIndex"); + return; + }; + + for &id in &p.entity_ids { + let Some(entity) = entity_id_index.remove(id) else { + debug!( + "Tried to remove entity with id {id} but it wasn't in the EntityIdIndex" + ); + continue; + }; + let Ok(mut loaded_by) = entity_query.get_mut(entity) else { + warn!( + "tried to despawn entity {id} but it doesn't have a LoadedBy component", + ); + continue; + }; + + // the [`remove_despawned_entities_from_indexes`] system will despawn the entity + // if it's not loaded by anything anymore + + // also we can't just ecs.despawn because if we're in a swarm then the entity + // might still be loaded by another client + + loaded_by.remove(&self.player); + } + }, + ); + } + pub fn player_chat(&mut self, p: &ClientboundPlayerChat) { + debug!("Got player chat packet {p:?}"); + + as_system::>(self.ecs, |mut events| { + events.send(ChatReceivedEvent { + entity: self.player, + packet: ChatPacket::Player(Arc::new(p.clone())), + }); + }); + } + + pub fn system_chat(&mut self, p: &ClientboundSystemChat) { + debug!("Got system chat packet {p:?}"); + + as_system::>(self.ecs, |mut events| { + events.send(ChatReceivedEvent { + entity: self.player, + packet: ChatPacket::System(Arc::new(p.clone())), + }); + }); + } + + pub fn disguised_chat(&mut self, p: &ClientboundDisguisedChat) { + debug!("Got disguised chat packet {p:?}"); + + as_system::>(self.ecs, |mut events| { + events.send(ChatReceivedEvent { + entity: self.player, + packet: ChatPacket::Disguised(Arc::new(p.clone())), + }); + }); + } + + pub fn sound(&mut self, _p: &ClientboundSound) {} + + pub fn level_event(&mut self, p: &ClientboundLevelEvent) { + debug!("Got level event packet {p:?}"); + } + + pub fn block_update(&mut self, p: &ClientboundBlockUpdate) { + debug!("Got block update packet {p:?}"); + + as_system::>(self.ecs, |mut query| { + let local_player = query.get_mut(self.player).unwrap(); + + let world = local_player.instance.write(); + + world.chunks.set_block_state(&p.pos, p.block_state); + }); + } + + pub fn animate(&mut self, p: &ClientboundAnimate) { + debug!("Got animate packet {p:?}"); + } + + pub fn section_blocks_update(&mut self, p: &ClientboundSectionBlocksUpdate) { + debug!("Got section blocks update packet {p:?}"); + + as_system::>(self.ecs, |mut query| { + let local_player = query.get_mut(self.player).unwrap(); + let world = local_player.instance.write(); + for state in &p.states { + world + .chunks + .set_block_state(&(p.section_pos + state.pos), state.state); + } + }); + } + + pub fn game_event(&mut self, p: &ClientboundGameEvent) { + use azalea_protocol::packets::game::c_game_event::EventType; + + debug!("Got game event packet {p:?}"); + + #[allow(clippy::single_match)] + match p.event { + EventType::ChangeGameMode => { + as_system::>(self.ecs, |mut query| { + let mut local_game_mode = query.get_mut(self.player).unwrap(); + if let Some(new_game_mode) = GameMode::from_id(p.param as u8) { + local_game_mode.current = new_game_mode; + } + }); + } + _ => {} + } + } + + pub fn level_particles(&mut self, p: &ClientboundLevelParticles) { + debug!("Got level particles packet {p:?}"); + } + + pub fn server_data(&mut self, p: &ClientboundServerData) { + debug!("Got server data packet {p:?}"); + } + + pub fn set_equipment(&mut self, p: &ClientboundSetEquipment) { + debug!("Got set equipment packet {p:?}"); + } + + pub fn update_mob_effect(&mut self, p: &ClientboundUpdateMobEffect) { + debug!("Got update mob effect packet {p:?}"); + } + + pub fn add_experience_orb(&mut self, _p: &ClientboundAddExperienceOrb) {} + + pub fn award_stats(&mut self, _p: &ClientboundAwardStats) {} + + pub fn block_changed_ack(&mut self, _p: &ClientboundBlockChangedAck) {} + + pub fn block_destruction(&mut self, _p: &ClientboundBlockDestruction) {} + + pub fn block_entity_data(&mut self, _p: &ClientboundBlockEntityData) {} + + pub fn block_event(&mut self, p: &ClientboundBlockEvent) { + debug!("Got block event packet {p:?}"); + } + + pub fn boss_event(&mut self, _p: &ClientboundBossEvent) {} + + pub fn command_suggestions(&mut self, _p: &ClientboundCommandSuggestions) {} + + pub fn container_set_content(&mut self, p: &ClientboundContainerSetContent) { + debug!("Got container set content packet {p:?}"); + + as_system::<(Query<&mut Inventory>, EventWriter<_>)>( + self.ecs, + |(mut query, mut events)| { + let mut inventory = query.get_mut(self.player).unwrap(); + + // container id 0 is always the player's inventory + if p.container_id == 0 { + // this is just so it has the same type as the `else` block + for (i, slot) in p.items.iter().enumerate() { + if let Some(slot_mut) = inventory.inventory_menu.slot_mut(i) { + *slot_mut = slot.clone(); + } + } + } else { + events.send(SetContainerContentEvent { + entity: self.player, + slots: p.items.clone(), + container_id: p.container_id, + }); + } + }, + ); + } + + pub fn container_set_data(&mut self, p: &ClientboundContainerSetData) { + debug!("Got container set data packet {p:?}"); + + // TODO: handle ContainerSetData packet + // this is used for various things like the furnace progress + // bar + // see https://wiki.vg/Protocol#Set_Container_Property + + // as_system::>(self.ecs, |mut query| { + // let inventory = query.get_mut(self.player).unwrap(); + // }); + } + + pub fn container_set_slot(&mut self, p: &ClientboundContainerSetSlot) { + debug!("Got container set slot packet {p:?}"); + + as_system::>(self.ecs, |mut query| { + let mut inventory = query.get_mut(self.player).unwrap(); + + if p.container_id == -1 { + // -1 means carried item + inventory.carried = p.item_stack.clone(); + } else if p.container_id == -2 { + if let Some(slot) = inventory.inventory_menu.slot_mut(p.slot.into()) { + *slot = p.item_stack.clone(); + } + } else { + let is_creative_mode_and_inventory_closed = false; + // technically minecraft has slightly different behavior here if you're in + // creative mode and have your inventory open + if p.container_id == 0 && azalea_inventory::Player::is_hotbar_slot(p.slot.into()) { + // minecraft also sets a "pop time" here which is used for an animation + // but that's not really necessary + if let Some(slot) = inventory.inventory_menu.slot_mut(p.slot.into()) { + *slot = p.item_stack.clone(); + } + } else if p.container_id == inventory.id + && (p.container_id != 0 || !is_creative_mode_and_inventory_closed) + { + // var2.containerMenu.setItem(var4, var1.getStateId(), var3); + if let Some(slot) = inventory.menu_mut().slot_mut(p.slot.into()) { + *slot = p.item_stack.clone(); + inventory.state_id = p.state_id; + } + } + } + }); + } + + pub fn container_close(&mut self, p: &ClientboundContainerClose) { + // there's a container_id field in the packet, but minecraft doesn't actually + // check it + + debug!("Got container close packet {p:?}"); + + as_system::>(self.ecs, |mut events| { + events.send(ClientSideCloseContainerEvent { + entity: self.player, + }); + }); + } + + pub fn cooldown(&mut self, _p: &ClientboundCooldown) {} + + pub fn custom_chat_completions(&mut self, _p: &ClientboundCustomChatCompletions) {} + + pub fn delete_chat(&mut self, _p: &ClientboundDeleteChat) {} + + pub fn explode(&mut self, p: &ClientboundExplode) { + trace!("Got explode packet {p:?}"); + + as_system::>(self.ecs, |mut knockback_events| { + if let Some(knockback) = p.knockback { + knockback_events.send(KnockbackEvent { + entity: self.player, + knockback: KnockbackType::Set(knockback), + }); + } + }); + } + + pub fn forget_level_chunk(&mut self, p: &ClientboundForgetLevelChunk) { + debug!("Got forget level chunk packet {p:?}"); + + as_system::>(self.ecs, |mut query| { + let local_player = query.get_mut(self.player).unwrap(); + + let mut partial_instance = local_player.partial_instance.write(); + + partial_instance.chunks.limited_set(&p.pos, None); + }); + } + + pub fn horse_screen_open(&mut self, _p: &ClientboundHorseScreenOpen) {} + + pub fn map_item_data(&mut self, _p: &ClientboundMapItemData) {} + + pub fn merchant_offers(&mut self, _p: &ClientboundMerchantOffers) {} + + pub fn move_vehicle(&mut self, _p: &ClientboundMoveVehicle) {} + + pub fn open_book(&mut self, _p: &ClientboundOpenBook) {} + + pub fn open_screen(&mut self, p: &ClientboundOpenScreen) { + debug!("Got open screen packet {p:?}"); + + as_system::>(self.ecs, |mut events| { + events.send(MenuOpenedEvent { + entity: self.player, + window_id: p.container_id, + menu_type: p.menu_type, + title: p.title.to_owned(), + }); + }); + } + + pub fn open_sign_editor(&mut self, _p: &ClientboundOpenSignEditor) {} + + pub fn ping(&mut self, p: &ClientboundPing) { + debug!("Got ping packet {p:?}"); + + as_system::>(self.ecs, |mut events| { + events.send(SendPacketEvent::new( + self.player, + ServerboundPong { id: p.id }, + )); + }); + } + + pub fn place_ghost_recipe(&mut self, _p: &ClientboundPlaceGhostRecipe) {} + + pub fn player_combat_end(&mut self, _p: &ClientboundPlayerCombatEnd) {} + + pub fn player_combat_enter(&mut self, _p: &ClientboundPlayerCombatEnter) {} + + pub fn player_combat_kill(&mut self, p: &ClientboundPlayerCombatKill) { + debug!("Got player kill packet {p:?}"); + + as_system::<( + Commands, + Query<(&MinecraftEntityId, Option<&Dead>)>, + EventWriter<_>, + )>(self.ecs, |(mut commands, mut query, mut events)| { + let (entity_id, dead) = query.get_mut(self.player).unwrap(); + + if *entity_id == p.player_id && dead.is_none() { + commands.entity(self.player).insert(Dead); + events.send(DeathEvent { + entity: self.player, + packet: Some(p.clone()), + }); + } + }); + } + + pub fn player_look_at(&mut self, _p: &ClientboundPlayerLookAt) {} + + pub fn remove_mob_effect(&mut self, _p: &ClientboundRemoveMobEffect) {} + + pub fn resource_pack_push(&mut self, p: &ClientboundResourcePackPush) { + debug!("Got resource pack packet {p:?}"); + + as_system::>(self.ecs, |mut events| { + events.send(ResourcePackEvent { + entity: self.player, + id: p.id, + url: p.url.to_owned(), + hash: p.hash.to_owned(), + required: p.required, + prompt: p.prompt.to_owned(), + }); + }); + } + + pub fn resource_pack_pop(&mut self, _p: &ClientboundResourcePackPop) {} + + pub fn respawn(&mut self, p: &ClientboundRespawn) { + debug!("Got respawn packet {p:?}"); + + as_system::<( + Commands, + Query<( + &mut InstanceHolder, + &GameProfileComponent, + &ClientInformation, + )>, + EventWriter<_>, + ResMut, + )>( + self.ecs, + |(mut commands, mut query, mut events, mut instance_container)| { + let (mut instance_holder, game_profile, client_information) = + query.get_mut(self.player).unwrap(); + + let new_instance_name = p.common.dimension.clone(); + + let Some((_dimension_type, dimension_data)) = p + .common + .dimension_type(&instance_holder.instance.read().registries) + else { + return; + }; + + // add this world to the instance_container (or don't if it's already + // there) + let weak_instance = instance_container.insert( + new_instance_name.clone(), + dimension_data.height, + dimension_data.min_y, + &instance_holder.instance.read().registries, + ); + events.send(InstanceLoadedEvent { + entity: self.player, + name: new_instance_name.clone(), + instance: Arc::downgrade(&weak_instance), + }); + + // set the partial_world to an empty world + // (when we add chunks or entities those will be in the + // instance_container) + + *instance_holder.partial_instance.write() = PartialInstance::new( + azalea_world::chunk_storage::calculate_chunk_storage_range( + client_information.view_distance.into(), + ), + Some(self.player), + ); + instance_holder.instance = weak_instance; + + // this resets a bunch of our components like physics and stuff + let entity_bundle = EntityBundle::new( + game_profile.uuid, + Vec3::default(), + azalea_registry::EntityKind::Player, + new_instance_name, + ); + // update the local gamemode and metadata things + commands.entity(self.player).insert(( + LocalGameMode { + current: p.common.game_type, + previous: p.common.previous_game_type.into(), + }, + entity_bundle, + )); + + // Remove the Dead marker component from the player. + commands.entity(self.player).remove::(); + }, + ) + } + + pub fn start_configuration(&mut self, _p: &ClientboundStartConfiguration) { + as_system::<(Commands, EventWriter<_>)>(self.ecs, |(mut commands, mut events)| { + events.send(SendPacketEvent::new( + self.player, + ServerboundConfigurationAcknowledged {}, + )); + + commands + .entity(self.player) + .insert(crate::client::InConfigState) + .remove::(); + }); + } + + pub fn entity_position_sync(&mut self, p: &ClientboundEntityPositionSync) { + as_system::<(Commands, Query<(&EntityIdIndex, &InstanceHolder)>)>( + self.ecs, + |(mut commands, mut query)| { + let (entity_id_index, instance_holder) = query.get_mut(self.player).unwrap(); + + let Some(entity) = entity_id_index.get(p.id) else { + debug!("Got teleport entity packet for unknown entity id {}", p.id); + return; + }; + + let new_position = p.values.pos; + let new_on_ground = p.on_ground; + let new_look_direction = p.values.look_direction; + + commands.entity(entity).queue(RelativeEntityUpdate { + partial_world: instance_holder.partial_instance.clone(), + update: Box::new(move |entity_mut| { + let is_local_entity = entity_mut.get::().is_some(); + let mut physics = entity_mut.get_mut::().unwrap(); + + physics.vec_delta_codec.set_base(new_position); + + if is_local_entity { + debug!("Ignoring entity position sync packet for local player"); + return; + } + + physics.set_on_ground(new_on_ground); + + let mut last_sent_position = + entity_mut.get_mut::().unwrap(); + **last_sent_position = new_position; + let mut position = entity_mut.get_mut::().unwrap(); + **position = new_position; + + let mut look_direction = entity_mut.get_mut::().unwrap(); + *look_direction = new_look_direction; + }), + }); + }, + ); + } + + pub fn select_advancements_tab(&mut self, _p: &ClientboundSelectAdvancementsTab) {} + pub fn set_action_bar_text(&mut self, _p: &ClientboundSetActionBarText) {} + pub fn set_border_center(&mut self, _p: &ClientboundSetBorderCenter) {} + pub fn set_border_lerp_size(&mut self, _p: &ClientboundSetBorderLerpSize) {} + pub fn set_border_size(&mut self, _p: &ClientboundSetBorderSize) {} + pub fn set_border_warning_delay(&mut self, _p: &ClientboundSetBorderWarningDelay) {} + pub fn set_border_warning_distance(&mut self, _p: &ClientboundSetBorderWarningDistance) {} + pub fn set_camera(&mut self, _p: &ClientboundSetCamera) {} + pub fn set_display_objective(&mut self, _p: &ClientboundSetDisplayObjective) {} + pub fn set_objective(&mut self, _p: &ClientboundSetObjective) {} + pub fn set_passengers(&mut self, _p: &ClientboundSetPassengers) {} + pub fn set_player_team(&mut self, _p: &ClientboundSetPlayerTeam) {} + pub fn set_score(&mut self, _p: &ClientboundSetScore) {} + pub fn set_simulation_distance(&mut self, _p: &ClientboundSetSimulationDistance) {} + pub fn set_subtitle_text(&mut self, _p: &ClientboundSetSubtitleText) {} + pub fn set_title_text(&mut self, _p: &ClientboundSetTitleText) {} + pub fn set_titles_animation(&mut self, _p: &ClientboundSetTitlesAnimation) {} + pub fn clear_titles(&mut self, _p: &ClientboundClearTitles) {} + pub fn sound_entity(&mut self, _p: &ClientboundSoundEntity) {} + pub fn stop_sound(&mut self, _p: &ClientboundStopSound) {} + pub fn tab_list(&mut self, _p: &ClientboundTabList) {} + pub fn tag_query(&mut self, _p: &ClientboundTagQuery) {} + pub fn take_item_entity(&mut self, _p: &ClientboundTakeItemEntity) {} + pub fn bundle_delimiter(&mut self, _p: &ClientboundBundleDelimiter) {} + pub fn damage_event(&mut self, _p: &ClientboundDamageEvent) {} + pub fn hurt_animation(&mut self, _p: &ClientboundHurtAnimation) {} + pub fn ticking_state(&mut self, _p: &ClientboundTickingState) {} + pub fn ticking_step(&mut self, _p: &ClientboundTickingStep) {} + pub fn reset_score(&mut self, _p: &ClientboundResetScore) {} + pub fn cookie_request(&mut self, _p: &ClientboundCookieRequest) {} + pub fn debug_sample(&mut self, _p: &ClientboundDebugSample) {} + pub fn pong_response(&mut self, _p: &ClientboundPongResponse) {} + pub fn store_cookie(&mut self, _p: &ClientboundStoreCookie) {} + pub fn transfer(&mut self, _p: &ClientboundTransfer) {} + pub fn move_minecart_along_track(&mut self, _p: &ClientboundMoveMinecartAlongTrack) {} + pub fn set_held_slot(&mut self, _p: &ClientboundSetHeldSlot) {} + pub fn set_player_inventory(&mut self, _p: &ClientboundSetPlayerInventory) {} + pub fn projectile_power(&mut self, _p: &ClientboundProjectilePower) {} + pub fn custom_report_details(&mut self, _p: &ClientboundCustomReportDetails) {} + pub fn server_links(&mut self, _p: &ClientboundServerLinks) {} + pub fn player_rotation(&mut self, _p: &ClientboundPlayerRotation) {} + pub fn recipe_book_add(&mut self, _p: &ClientboundRecipeBookAdd) {} + pub fn recipe_book_remove(&mut self, _p: &ClientboundRecipeBookRemove) {} + pub fn recipe_book_settings(&mut self, _p: &ClientboundRecipeBookSettings) {} +} diff --git a/azalea-client/src/plugins/packet/login.rs b/azalea-client/src/plugins/packet/login.rs new file mode 100644 index 00000000..1bb07266 --- /dev/null +++ b/azalea-client/src/plugins/packet/login.rs @@ -0,0 +1,114 @@ +// login packets aren't actually handled here because compression/encryption +// would make packet handling a lot messier + +use std::{collections::HashSet, sync::Arc}; + +use azalea_protocol::packets::{ + Packet, + login::{ + ClientboundLoginPacket, ServerboundLoginPacket, + s_custom_query_answer::ServerboundCustomQueryAnswer, + }, +}; +use bevy_ecs::{prelude::*, system::SystemState}; +use derive_more::{Deref, DerefMut}; +use tokio::sync::mpsc; +use tracing::error; + +// this struct is defined here anyways though so it's consistent with the other +// ones + +/// An event that's sent when we receive a login packet from the server. Note +/// that if you want to handle this in a system, you must add +/// `.before(azalea::packet::login::process_packet_events)` to it +/// because that system clears the events. +#[derive(Event, Debug, Clone)] +pub struct LoginPacketEvent { + /// The client entity that received the packet. + pub entity: Entity, + /// The packet that was actually received. + pub packet: Arc, +} + +/// Event for sending a login packet to the server. +#[derive(Event)] +pub struct SendLoginPacketEvent { + pub entity: Entity, + pub packet: ServerboundLoginPacket, +} +impl SendLoginPacketEvent { + pub fn new(entity: Entity, packet: impl Packet) -> Self { + let packet = packet.into_variant(); + Self { entity, packet } + } +} + +#[derive(Component)] +pub struct LoginSendPacketQueue { + pub tx: mpsc::UnboundedSender, +} + +/// A marker component for local players that are currently in the +/// `login` state. +#[derive(Component, Clone, Debug)] +pub struct InLoginState; + +pub fn handle_send_packet_event( + mut send_packet_events: EventReader, + mut query: Query<&mut LoginSendPacketQueue>, +) { + for event in send_packet_events.read() { + if let Ok(queue) = query.get_mut(event.entity) { + let _ = queue.tx.send(event.packet.clone()); + } else { + error!("Sent SendPacketEvent for entity that doesn't have a LoginSendPacketQueue"); + } + } +} + +/// Plugins can add to this set if they want to handle a custom query packet +/// themselves. This component removed after the login state ends. +#[derive(Component, Default, Debug, Deref, DerefMut)] +pub struct IgnoreQueryIds(HashSet); + +pub fn process_packet_events(ecs: &mut World) { + let mut events_owned = Vec::new(); + let mut system_state: SystemState>> = SystemState::new(ecs); + let mut events = system_state.get_mut(ecs); + for LoginPacketEvent { + entity: player_entity, + packet, + } in events.drain() + { + // we do this so `ecs` isn't borrowed for the whole loop + events_owned.push((player_entity, packet)); + } + for (player_entity, packet) in events_owned { + #[allow(clippy::single_match)] + match packet.as_ref() { + ClientboundLoginPacket::CustomQuery(p) => { + let mut system_state: SystemState<( + EventWriter, + Query<&IgnoreQueryIds>, + )> = SystemState::new(ecs); + let (mut send_packet_events, query) = system_state.get_mut(ecs); + + let ignore_query_ids = query.get(player_entity).ok().map(|x| x.0.clone()); + if let Some(ignore_query_ids) = ignore_query_ids { + if ignore_query_ids.contains(&p.transaction_id) { + continue; + } + } + + send_packet_events.send(SendLoginPacketEvent::new( + player_entity, + ServerboundCustomQueryAnswer { + transaction_id: p.transaction_id, + data: None, + }, + )); + } + _ => {} + } + } +} diff --git a/azalea-client/src/plugins/packet/mod.rs b/azalea-client/src/plugins/packet/mod.rs new file mode 100644 index 00000000..cbd8a175 --- /dev/null +++ b/azalea-client/src/plugins/packet/mod.rs @@ -0,0 +1,109 @@ +use azalea_entity::{EntityUpdateSet, metadata::Health}; +use bevy_app::{App, First, Plugin, PreUpdate, Update}; +use bevy_ecs::{ + prelude::*, + system::{SystemParam, SystemState}, +}; + +use self::{ + game::{ + AddPlayerEvent, DeathEvent, InstanceLoadedEvent, KeepAliveEvent, RemovePlayerEvent, + ResourcePackEvent, UpdatePlayerEvent, + }, + login::{LoginPacketEvent, SendLoginPacketEvent}, +}; +use crate::{chat::ChatReceivedEvent, events::death_listener}; + +pub mod config; +pub mod game; +pub mod login; + +pub struct PacketPlugin; + +pub fn death_event_on_0_health( + query: Query<(Entity, &Health), Changed>, + mut death_events: EventWriter, +) { + for (entity, health) in query.iter() { + if **health == 0. { + death_events.send(DeathEvent { + entity, + packet: None, + }); + } + } +} + +impl Plugin for PacketPlugin { + fn build(&self, app: &mut App) { + app.add_systems( + First, + (game::send_receivepacketevent, config::send_packet_events), + ) + .add_systems( + PreUpdate, + ( + game::process_packet_events + // we want to index and deindex right after + .before(EntityUpdateSet::Deindex), + config::process_packet_events, + login::handle_send_packet_event, + login::process_packet_events, + ), + ) + .add_systems( + Update, + ( + ( + config::handle_send_packet_event, + game::handle_outgoing_packets, + ) + .chain(), + death_event_on_0_health.before(death_listener), + ), + ) + // we do this instead of add_event so we can handle the events ourselves + .init_resource::>() + .init_resource::>() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::(); + } +} + +#[macro_export] +macro_rules! declare_packet_handlers { + ( + $packetenum:ident, + $packetvar:expr, + $handler:ident, + [$($packet:path),+ $(,)?] + ) => { + paste::paste! { + match $packetvar { + $( + $packetenum::[< $packet:camel >](p) => $handler.$packet(p), + )+ + } + } + }; +} + +pub(crate) fn as_system(ecs: &mut World, f: impl FnOnce(T::Item<'_, '_>)) +where + T: SystemParam + 'static, +{ + let mut system_state = SystemState::::new(ecs); + let values = system_state.get_mut(ecs); + f(values); + system_state.apply(ecs); +} diff --git a/azalea-client/src/plugins/respawn.rs b/azalea-client/src/plugins/respawn.rs new file mode 100644 index 00000000..5797406b --- /dev/null +++ b/azalea-client/src/plugins/respawn.rs @@ -0,0 +1,35 @@ +use azalea_protocol::packets::game::s_client_command::{self, ServerboundClientCommand}; +use bevy_app::{App, Plugin, Update}; +use bevy_ecs::prelude::*; + +use super::packet::game::handle_outgoing_packets; +use crate::packet::game::SendPacketEvent; + +/// Tell the server that we're respawning. +#[derive(Event, Debug, Clone)] +pub struct PerformRespawnEvent { + pub entity: Entity, +} + +/// A plugin that makes [`PerformRespawnEvent`] send the packet to respawn. +pub struct RespawnPlugin; +impl Plugin for RespawnPlugin { + fn build(&self, app: &mut App) { + app.add_event::() + .add_systems(Update, perform_respawn.before(handle_outgoing_packets)); + } +} + +pub fn perform_respawn( + mut events: EventReader, + mut send_packets: EventWriter, +) { + for event in events.read() { + send_packets.send(SendPacketEvent::new( + event.entity, + ServerboundClientCommand { + action: s_client_command::Action::PerformRespawn, + }, + )); + } +} diff --git a/azalea-client/src/plugins/task_pool.rs b/azalea-client/src/plugins/task_pool.rs new file mode 100644 index 00000000..ab56bf69 --- /dev/null +++ b/azalea-client/src/plugins/task_pool.rs @@ -0,0 +1,182 @@ +//! Borrowed from `bevy_core`. + +use std::marker::PhantomData; + +use bevy_app::{App, Last, Plugin}; +use bevy_ecs::system::{NonSend, Resource}; +use bevy_tasks::{ + AsyncComputeTaskPool, ComputeTaskPool, IoTaskPool, TaskPoolBuilder, + tick_global_task_pools_on_main_thread, +}; + +/// Setup of default task pools: `AsyncComputeTaskPool`, `ComputeTaskPool`, +/// `IoTaskPool`. +#[derive(Default)] +pub struct TaskPoolPlugin { + /// Options for the [`TaskPool`](bevy_tasks::TaskPool) created at + /// application start. + pub task_pool_options: TaskPoolOptions, +} + +impl Plugin for TaskPoolPlugin { + fn build(&self, app: &mut App) { + // Setup the default bevy task pools + self.task_pool_options.create_default_pools(); + + #[cfg(not(target_arch = "wasm32"))] + app.add_systems(Last, tick_global_task_pools); + } +} + +pub struct NonSendMarker(PhantomData<*mut ()>); +#[cfg(not(target_arch = "wasm32"))] +fn tick_global_task_pools(_main_thread_marker: Option>) { + tick_global_task_pools_on_main_thread(); +} + +/// Helper for configuring and creating the default task pools. For end-users +/// who want full control, set up [`TaskPoolPlugin`] +#[derive(Clone, Resource)] +pub struct TaskPoolOptions { + /// If the number of physical cores is less than min_total_threads, force + /// using min_total_threads + pub min_total_threads: usize, + /// If the number of physical cores is greater than max_total_threads, force + /// using max_total_threads + pub max_total_threads: usize, + + /// Used to determine number of IO threads to allocate + pub io: TaskPoolThreadAssignmentPolicy, + /// Used to determine number of async compute threads to allocate + pub async_compute: TaskPoolThreadAssignmentPolicy, + /// Used to determine number of compute threads to allocate + pub compute: TaskPoolThreadAssignmentPolicy, +} + +impl Default for TaskPoolOptions { + fn default() -> Self { + TaskPoolOptions { + // By default, use however many cores are available on the system + min_total_threads: 1, + max_total_threads: usize::MAX, + + // Use 25% of cores for IO, at least 1, no more than 4 + io: TaskPoolThreadAssignmentPolicy { + min_threads: 1, + max_threads: 4, + percent: 0.25, + }, + + // Use 25% of cores for async compute, at least 1, no more than 4 + async_compute: TaskPoolThreadAssignmentPolicy { + min_threads: 1, + max_threads: 4, + percent: 0.25, + }, + + // Use all remaining cores for compute (at least 1) + compute: TaskPoolThreadAssignmentPolicy { + min_threads: 1, + max_threads: usize::MAX, + percent: 1.0, // This 1.0 here means "whatever is left over" + }, + } + } +} + +impl TaskPoolOptions { + // /// Create a configuration that forces using the given number of threads. + // pub fn with_num_threads(thread_count: usize) -> Self { + // TaskPoolOptions { + // min_total_threads: thread_count, + // max_total_threads: thread_count, + // ..Default::default() + // } + // } + + /// Inserts the default thread pools into the given resource map based on + /// the configured values + pub fn create_default_pools(&self) { + let total_threads = bevy_tasks::available_parallelism() + .clamp(self.min_total_threads, self.max_total_threads); + + let mut remaining_threads = total_threads; + + { + // Determine the number of IO threads we will use + let io_threads = self + .io + .get_number_of_threads(remaining_threads, total_threads); + + remaining_threads = remaining_threads.saturating_sub(io_threads); + + IoTaskPool::get_or_init(|| { + TaskPoolBuilder::default() + .num_threads(io_threads) + .thread_name("IO Task Pool".to_string()) + .build() + }); + } + + { + // Determine the number of async compute threads we will use + let async_compute_threads = self + .async_compute + .get_number_of_threads(remaining_threads, total_threads); + + remaining_threads = remaining_threads.saturating_sub(async_compute_threads); + + AsyncComputeTaskPool::get_or_init(|| { + TaskPoolBuilder::default() + .num_threads(async_compute_threads) + .thread_name("Async Compute Task Pool".to_string()) + .build() + }); + } + + { + // Determine the number of compute threads we will use + // This is intentionally last so that an end user can specify 1.0 as the percent + let compute_threads = self + .compute + .get_number_of_threads(remaining_threads, total_threads); + + ComputeTaskPool::get_or_init(|| { + TaskPoolBuilder::default() + .num_threads(compute_threads) + .thread_name("Compute Task Pool".to_string()) + .build() + }); + } + } +} + +/// Defines a simple way to determine how many threads to use given the number +/// of remaining cores and number of total cores +#[derive(Clone)] +pub struct TaskPoolThreadAssignmentPolicy { + /// Force using at least this many threads + pub min_threads: usize, + /// Under no circumstance use more than this many threads for this pool + pub max_threads: usize, + /// Target using this percentage of total cores, clamped by min_threads and + /// max_threads. It is permitted to use 1.0 to try to use all remaining + /// threads + pub percent: f32, +} + +impl TaskPoolThreadAssignmentPolicy { + /// Determine the number of threads to use for this task pool + fn get_number_of_threads(&self, remaining_threads: usize, total_threads: usize) -> usize { + assert!(self.percent >= 0.0); + let mut desired = (total_threads as f32 * self.percent).round() as usize; + + // Limit ourselves to the number of cores available + desired = desired.min(remaining_threads); + + // Clamp by min_threads, max_threads. (This may result in us using more threads + // than are available, this is intended. An example case where this + // might happen is a device with <= 2 threads. + desired.clamp(self.min_threads, self.max_threads) + } +} diff --git a/azalea-client/src/plugins/tick_end.rs b/azalea-client/src/plugins/tick_end.rs new file mode 100644 index 00000000..c7737eb1 --- /dev/null +++ b/azalea-client/src/plugins/tick_end.rs @@ -0,0 +1,36 @@ +//! Clients send a [`ServerboundClientTickEnd`] packet every tick. + +use azalea_core::tick::GameTick; +use azalea_entity::LocalEntity; +use azalea_physics::PhysicsSet; +use azalea_protocol::packets::game::ServerboundClientTickEnd; +use azalea_world::InstanceName; +use bevy_app::{App, Plugin}; +use bevy_ecs::prelude::*; + +use crate::{mining::MiningSet, packet::game::SendPacketEvent}; + +/// A plugin that makes clients send a [`ServerboundClientTickEnd`] packet every +/// tick. +pub struct TickEndPlugin; +impl Plugin for TickEndPlugin { + fn build(&self, app: &mut App) { + app.add_systems( + GameTick, + // this has to happen after every other event that might send packets + game_tick_packet + .after(PhysicsSet) + .after(MiningSet) + .after(crate::movement::send_position), + ); + } +} + +pub fn game_tick_packet( + query: Query, With)>, + mut send_packets: EventWriter, +) { + for entity in query.iter() { + send_packets.send(SendPacketEvent::new(entity, ServerboundClientTickEnd)); + } +} diff --git a/azalea-client/src/respawn.rs b/azalea-client/src/respawn.rs deleted file mode 100644 index 41d1c470..00000000 --- a/azalea-client/src/respawn.rs +++ /dev/null @@ -1,34 +0,0 @@ -use azalea_protocol::packets::game::s_client_command::{self, ServerboundClientCommand}; -use bevy_app::{App, Plugin, Update}; -use bevy_ecs::prelude::*; - -use crate::packet_handling::game::{SendPacketEvent, handle_send_packet_event}; - -/// Tell the server that we're respawning. -#[derive(Event, Debug, Clone)] -pub struct PerformRespawnEvent { - pub entity: Entity, -} - -/// A plugin that makes [`PerformRespawnEvent`] send the packet to respawn. -pub struct RespawnPlugin; -impl Plugin for RespawnPlugin { - fn build(&self, app: &mut App) { - app.add_event::() - .add_systems(Update, perform_respawn.before(handle_send_packet_event)); - } -} - -pub fn perform_respawn( - mut events: EventReader, - mut send_packets: EventWriter, -) { - for event in events.read() { - send_packets.send(SendPacketEvent::new( - event.entity, - ServerboundClientCommand { - action: s_client_command::Action::PerformRespawn, - }, - )); - } -} diff --git a/azalea-client/src/send_client_end.rs b/azalea-client/src/send_client_end.rs deleted file mode 100644 index cb3d5e74..00000000 --- a/azalea-client/src/send_client_end.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! Clients send a [`ServerboundClientTickEnd`] packet every tick. - -use azalea_core::tick::GameTick; -use azalea_entity::LocalEntity; -use azalea_physics::PhysicsSet; -use azalea_protocol::packets::game::ServerboundClientTickEnd; -use azalea_world::InstanceName; -use bevy_app::{App, Plugin}; -use bevy_ecs::prelude::*; - -use crate::{mining::MiningSet, packet_handling::game::SendPacketEvent}; - -/// A plugin that makes clients send a [`ServerboundClientTickEnd`] packet every -/// tick. -pub struct TickEndPlugin; -impl Plugin for TickEndPlugin { - fn build(&self, app: &mut App) { - app.add_systems( - GameTick, - // this has to happen after every other event that might send packets - game_tick_packet - .after(PhysicsSet) - .after(MiningSet) - .after(crate::movement::send_position), - ); - } -} - -pub fn game_tick_packet( - query: Query, With)>, - mut send_packets: EventWriter, -) { - for entity in query.iter() { - send_packets.send(SendPacketEvent::new(entity, ServerboundClientTickEnd)); - } -} diff --git a/azalea-client/src/task_pool.rs b/azalea-client/src/task_pool.rs deleted file mode 100644 index ab56bf69..00000000 --- a/azalea-client/src/task_pool.rs +++ /dev/null @@ -1,182 +0,0 @@ -//! Borrowed from `bevy_core`. - -use std::marker::PhantomData; - -use bevy_app::{App, Last, Plugin}; -use bevy_ecs::system::{NonSend, Resource}; -use bevy_tasks::{ - AsyncComputeTaskPool, ComputeTaskPool, IoTaskPool, TaskPoolBuilder, - tick_global_task_pools_on_main_thread, -}; - -/// Setup of default task pools: `AsyncComputeTaskPool`, `ComputeTaskPool`, -/// `IoTaskPool`. -#[derive(Default)] -pub struct TaskPoolPlugin { - /// Options for the [`TaskPool`](bevy_tasks::TaskPool) created at - /// application start. - pub task_pool_options: TaskPoolOptions, -} - -impl Plugin for TaskPoolPlugin { - fn build(&self, app: &mut App) { - // Setup the default bevy task pools - self.task_pool_options.create_default_pools(); - - #[cfg(not(target_arch = "wasm32"))] - app.add_systems(Last, tick_global_task_pools); - } -} - -pub struct NonSendMarker(PhantomData<*mut ()>); -#[cfg(not(target_arch = "wasm32"))] -fn tick_global_task_pools(_main_thread_marker: Option>) { - tick_global_task_pools_on_main_thread(); -} - -/// Helper for configuring and creating the default task pools. For end-users -/// who want full control, set up [`TaskPoolPlugin`] -#[derive(Clone, Resource)] -pub struct TaskPoolOptions { - /// If the number of physical cores is less than min_total_threads, force - /// using min_total_threads - pub min_total_threads: usize, - /// If the number of physical cores is greater than max_total_threads, force - /// using max_total_threads - pub max_total_threads: usize, - - /// Used to determine number of IO threads to allocate - pub io: TaskPoolThreadAssignmentPolicy, - /// Used to determine number of async compute threads to allocate - pub async_compute: TaskPoolThreadAssignmentPolicy, - /// Used to determine number of compute threads to allocate - pub compute: TaskPoolThreadAssignmentPolicy, -} - -impl Default for TaskPoolOptions { - fn default() -> Self { - TaskPoolOptions { - // By default, use however many cores are available on the system - min_total_threads: 1, - max_total_threads: usize::MAX, - - // Use 25% of cores for IO, at least 1, no more than 4 - io: TaskPoolThreadAssignmentPolicy { - min_threads: 1, - max_threads: 4, - percent: 0.25, - }, - - // Use 25% of cores for async compute, at least 1, no more than 4 - async_compute: TaskPoolThreadAssignmentPolicy { - min_threads: 1, - max_threads: 4, - percent: 0.25, - }, - - // Use all remaining cores for compute (at least 1) - compute: TaskPoolThreadAssignmentPolicy { - min_threads: 1, - max_threads: usize::MAX, - percent: 1.0, // This 1.0 here means "whatever is left over" - }, - } - } -} - -impl TaskPoolOptions { - // /// Create a configuration that forces using the given number of threads. - // pub fn with_num_threads(thread_count: usize) -> Self { - // TaskPoolOptions { - // min_total_threads: thread_count, - // max_total_threads: thread_count, - // ..Default::default() - // } - // } - - /// Inserts the default thread pools into the given resource map based on - /// the configured values - pub fn create_default_pools(&self) { - let total_threads = bevy_tasks::available_parallelism() - .clamp(self.min_total_threads, self.max_total_threads); - - let mut remaining_threads = total_threads; - - { - // Determine the number of IO threads we will use - let io_threads = self - .io - .get_number_of_threads(remaining_threads, total_threads); - - remaining_threads = remaining_threads.saturating_sub(io_threads); - - IoTaskPool::get_or_init(|| { - TaskPoolBuilder::default() - .num_threads(io_threads) - .thread_name("IO Task Pool".to_string()) - .build() - }); - } - - { - // Determine the number of async compute threads we will use - let async_compute_threads = self - .async_compute - .get_number_of_threads(remaining_threads, total_threads); - - remaining_threads = remaining_threads.saturating_sub(async_compute_threads); - - AsyncComputeTaskPool::get_or_init(|| { - TaskPoolBuilder::default() - .num_threads(async_compute_threads) - .thread_name("Async Compute Task Pool".to_string()) - .build() - }); - } - - { - // Determine the number of compute threads we will use - // This is intentionally last so that an end user can specify 1.0 as the percent - let compute_threads = self - .compute - .get_number_of_threads(remaining_threads, total_threads); - - ComputeTaskPool::get_or_init(|| { - TaskPoolBuilder::default() - .num_threads(compute_threads) - .thread_name("Compute Task Pool".to_string()) - .build() - }); - } - } -} - -/// Defines a simple way to determine how many threads to use given the number -/// of remaining cores and number of total cores -#[derive(Clone)] -pub struct TaskPoolThreadAssignmentPolicy { - /// Force using at least this many threads - pub min_threads: usize, - /// Under no circumstance use more than this many threads for this pool - pub max_threads: usize, - /// Target using this percentage of total cores, clamped by min_threads and - /// max_threads. It is permitted to use 1.0 to try to use all remaining - /// threads - pub percent: f32, -} - -impl TaskPoolThreadAssignmentPolicy { - /// Determine the number of threads to use for this task pool - fn get_number_of_threads(&self, remaining_threads: usize, total_threads: usize) -> usize { - assert!(self.percent >= 0.0); - let mut desired = (total_threads as f32 * self.percent).round() as usize; - - // Limit ourselves to the number of cores available - desired = desired.min(remaining_threads); - - // Clamp by min_threads, max_threads. (This may result in us using more threads - // than are available, this is intended. An example case where this - // might happen is a device with <= 2 threads. - desired.clamp(self.min_threads, self.max_threads) - } -} diff --git a/azalea-entity/src/lib.rs b/azalea-entity/src/lib.rs index 1024ffe6..770afc50 100644 --- a/azalea-entity/src/lib.rs +++ b/azalea-entity/src/lib.rs @@ -482,7 +482,7 @@ impl EntityBundle { /// be updated by other clients. /// /// If this is for a client then all of our clients will have this. -#[derive(Component, Clone, Debug)] +#[derive(Component, Clone, Debug, Default)] pub struct LocalEntity; #[derive(Component, Clone, Debug, PartialEq, Deref, DerefMut)] diff --git a/azalea-protocol/src/packets/game/c_add_entity.rs b/azalea-protocol/src/packets/game/c_add_entity.rs index 67615fb9..28f86a3e 100755 --- a/azalea-protocol/src/packets/game/c_add_entity.rs +++ b/azalea-protocol/src/packets/game/c_add_entity.rs @@ -1,5 +1,5 @@ use azalea_buf::AzBuf; -use azalea_core::{position::Vec3, resource_location::ResourceLocation}; +use azalea_core::{delta::PositionDelta8, position::Vec3, resource_location::ResourceLocation}; use azalea_entity::{metadata::apply_default_metadata, EntityBundle}; use azalea_protocol_macros::ClientboundGamePacket; use azalea_world::MinecraftEntityId; @@ -18,9 +18,7 @@ pub struct ClientboundAddEntity { pub y_head_rot: i8, #[var] pub data: u32, - pub x_vel: i16, - pub y_vel: i16, - pub z_vel: i16, + pub velocity: PositionDelta8, } impl ClientboundAddEntity { diff --git a/azalea-protocol/src/packets/game/c_set_entity_motion.rs b/azalea-protocol/src/packets/game/c_set_entity_motion.rs index 7a112784..06b457f7 100755 --- a/azalea-protocol/src/packets/game/c_set_entity_motion.rs +++ b/azalea-protocol/src/packets/game/c_set_entity_motion.rs @@ -1,4 +1,5 @@ use azalea_buf::AzBuf; +use azalea_core::delta::PositionDelta8; use azalea_protocol_macros::ClientboundGamePacket; use azalea_world::MinecraftEntityId; @@ -6,7 +7,5 @@ use azalea_world::MinecraftEntityId; pub struct ClientboundSetEntityMotion { #[var] pub id: MinecraftEntityId, - pub xa: i16, - pub ya: i16, - pub za: i16, + pub delta: PositionDelta8, } diff --git a/azalea/examples/testbot/commands/debug.rs b/azalea/examples/testbot/commands/debug.rs index df98511d..1b3b2d61 100644 --- a/azalea/examples/testbot/commands/debug.rs +++ b/azalea/examples/testbot/commands/debug.rs @@ -8,7 +8,7 @@ use azalea::{ chunks::ReceiveChunkEvent, entity::{LookDirection, Position}, interact::HitResultComponent, - packet_handling::game, + packet::game, pathfinder::{ExecutingPath, Pathfinder}, world::MinecraftEntityId, }; @@ -240,8 +240,8 @@ pub fn register(commands: &mut CommandDispatcher>) { } } } - "bevy_ecs::event::collections::Events" => { - let events = ecs.resource::>(); + "bevy_ecs::event::collections::Events" => { + let events = ecs.resource::>(); writeln!(report, "- Event count: {}", events.len()).unwrap(); } "bevy_ecs::event::collections::Events" => { diff --git a/azalea/src/accept_resource_packs.rs b/azalea/src/accept_resource_packs.rs index f62d5ec0..13deef8e 100644 --- a/azalea/src/accept_resource_packs.rs +++ b/azalea/src/accept_resource_packs.rs @@ -1,7 +1,7 @@ use azalea_client::chunks::handle_chunk_batch_finished_event; use azalea_client::inventory::InventorySet; -use azalea_client::packet_handling::game::SendPacketEvent; -use azalea_client::packet_handling::{death_event_on_0_health, game::ResourcePackEvent}; +use azalea_client::packet::game::SendPacketEvent; +use azalea_client::packet::{death_event_on_0_health, game::ResourcePackEvent}; use azalea_client::respawn::perform_respawn; use azalea_protocol::packets::game::s_resource_pack::{self, ServerboundResourcePack}; use bevy_app::Update; diff --git a/azalea/src/auto_respawn.rs b/azalea/src/auto_respawn.rs index 191e6df7..0d878595 100644 --- a/azalea/src/auto_respawn.rs +++ b/azalea/src/auto_respawn.rs @@ -1,5 +1,5 @@ use azalea_client::{ - packet_handling::{death_event_on_0_health, game::DeathEvent}, + packet::{death_event_on_0_health, game::DeathEvent}, respawn::{PerformRespawnEvent, perform_respawn}, }; use bevy_app::Update; diff --git a/azalea/src/container.rs b/azalea/src/container.rs index e1a018b0..b5ed74cc 100644 --- a/azalea/src/container.rs +++ b/azalea/src/container.rs @@ -1,10 +1,10 @@ use std::fmt::Debug; use std::fmt::Formatter; +use azalea_client::packet::game::ReceivePacketEvent; use azalea_client::{ Client, inventory::{CloseContainerEvent, ContainerClickEvent, Inventory}, - packet_handling::game::PacketEvent, }; use azalea_core::position::BlockPos; use azalea_inventory::{ItemStack, Menu, operations::ClickOperation}; @@ -234,7 +234,7 @@ impl ContainerHandle { #[derive(Component, Debug)] pub struct WaitingForInventoryOpen; -fn handle_menu_opened_event(mut commands: Commands, mut events: EventReader) { +fn handle_menu_opened_event(mut commands: Commands, mut events: EventReader) { for event in events.read() { if let ClientboundGamePacket::ContainerSetContent { .. } = event.packet.as_ref() { commands diff --git a/azalea/src/pathfinder/simulation.rs b/azalea/src/pathfinder/simulation.rs index ab0e540a..5a68bf88 100644 --- a/azalea/src/pathfinder/simulation.rs +++ b/azalea/src/pathfinder/simulation.rs @@ -2,7 +2,7 @@ use std::sync::Arc; -use azalea_client::{PhysicsState, inventory::Inventory, packet_handling::game::SendPacketEvent}; +use azalea_client::{PhysicsState, inventory::Inventory, packet::game::SendPacketEvent}; use azalea_core::{position::Vec3, resource_location::ResourceLocation, tick::GameTick}; use azalea_entity::{ Attributes, EntityDimensions, LookDirection, Physics, Position, attributes::AttributeInstance, @@ -60,13 +60,13 @@ fn create_simulation_instance(chunks: ChunkStorage) -> (App, Arc