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: [PATCH] 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/client.rs | 37 +- azalea-client/src/lib.rs | 16 +- azalea-client/src/local_player.rs | 13 +- .../src/packet_handling/configuration.rs | 271 --- azalea-client/src/packet_handling/game.rs | 1585 ----------------- azalea-client/src/player.rs | 2 +- azalea-client/src/{ => plugins}/attack.rs | 3 +- .../{configuration.rs => plugins/brand.rs} | 19 +- azalea-client/src/plugins/chat/handler.rs | 61 + .../src/{chat.rs => plugins/chat/mod.rs} | 111 +- azalea-client/src/{ => plugins}/chunks.rs | 14 +- azalea-client/src/{ => plugins}/disconnect.rs | 0 azalea-client/src/{ => plugins}/events.rs | 54 +- azalea-client/src/{ => plugins}/interact.rs | 5 +- azalea-client/src/{ => plugins}/inventory.rs | 8 +- azalea-client/src/{ => plugins}/mining.rs | 7 +- azalea-client/src/plugins/mod.rs | 14 + azalea-client/src/{ => plugins}/movement.rs | 6 +- .../src/plugins/packet/config/events.rs | 90 + .../src/plugins/packet/config/mod.rs | 223 +++ .../src/plugins/packet/game/events.rs | 178 ++ azalea-client/src/plugins/packet/game/mod.rs | 1583 ++++++++++++++++ .../packet}/login.rs | 2 +- .../packet}/mod.rs | 53 +- azalea-client/src/{ => plugins}/respawn.rs | 5 +- azalea-client/src/{ => plugins}/task_pool.rs | 0 .../tick_end.rs} | 2 +- azalea-entity/src/lib.rs | 2 +- .../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 +- 39 files changed, 2342 insertions(+), 2087 deletions(-) delete mode 100644 azalea-client/src/packet_handling/configuration.rs delete mode 100644 azalea-client/src/packet_handling/game.rs rename azalea-client/src/{ => plugins}/attack.rs (98%) rename azalea-client/src/{configuration.rs => plugins/brand.rs} (74%) create mode 100644 azalea-client/src/plugins/chat/handler.rs rename azalea-client/src/{chat.rs => plugins/chat/mod.rs} (75%) mode change 100755 => 100644 rename azalea-client/src/{ => plugins}/chunks.rs (95%) rename azalea-client/src/{ => plugins}/disconnect.rs (100%) rename azalea-client/src/{ => plugins}/events.rs (80%) rename azalea-client/src/{ => plugins}/interact.rs (98%) rename azalea-client/src/{ => plugins}/inventory.rs (99%) rename azalea-client/src/{ => plugins}/mining.rs (99%) create mode 100644 azalea-client/src/plugins/mod.rs rename azalea-client/src/{ => plugins}/movement.rs (99%) 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 rename azalea-client/src/{packet_handling => plugins/packet}/login.rs (98%) rename azalea-client/src/{packet_handling => plugins/packet}/mod.rs (61%) rename azalea-client/src/{ => plugins}/respawn.rs (83%) rename azalea-client/src/{ => plugins}/task_pool.rs (100%) rename azalea-client/src/{send_client_end.rs => plugins/tick_end.rs} (93%) 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/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/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/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/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/attack.rs b/azalea-client/src/plugins/attack.rs similarity index 98% rename from azalea-client/src/attack.rs rename to azalea-client/src/plugins/attack.rs index 0f5a8305..1b2bc1ee 100644 --- a/azalea-client/src/attack.rs +++ b/azalea-client/src/plugins/attack.rs @@ -11,9 +11,10 @@ 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, - packet_handling::game::SendPacketEvent, respawn::perform_respawn, + respawn::perform_respawn, }; pub struct AttackPlugin; diff --git a/azalea-client/src/configuration.rs b/azalea-client/src/plugins/brand.rs similarity index 74% rename from azalea-client/src/configuration.rs rename to azalea-client/src/plugins/brand.rs index d578be7a..e15a6c67 100644 --- a/azalea-client/src/configuration.rs +++ b/azalea-client/src/plugins/brand.rs @@ -11,15 +11,15 @@ use bevy_app::prelude::*; use bevy_ecs::prelude::*; use tracing::{debug, warn}; -use crate::packet_handling::{configuration::SendConfigurationEvent, login::InLoginState}; +use super::packet::config::SendConfigPacketEvent; +use crate::packet::login::InLoginState; -pub struct ConfigurationPlugin; -impl Plugin for ConfigurationPlugin { +pub struct BrandPlugin; +impl Plugin for BrandPlugin { fn build(&self, app: &mut App) { app.add_systems( Update, - handle_end_login_state - .before(crate::packet_handling::configuration::handle_send_packet_event), + handle_end_login_state.before(crate::packet::config::handle_send_packet_event), ); } } @@ -27,13 +27,14 @@ impl Plugin for ConfigurationPlugin { fn handle_end_login_state( mut removed: RemovedComponents, query: Query<&ClientInformation>, - mut send_packet_events: EventWriter, + mut send_packet_events: EventWriter, ) { for entity in removed.read() { let mut brand_data = Vec::new(); - // they don't have to know :) + // 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(SendConfigurationEvent::new( + send_packet_events.send(SendConfigPacketEvent::new( entity, ServerboundCustomPayload { identifier: ResourceLocation::new("brand"), @@ -52,7 +53,7 @@ fn handle_end_login_state( }; debug!("Writing ClientInformation while in config state: {client_information:?}"); - send_packet_events.send(SendConfigurationEvent::new( + 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/chat.rs b/azalea-client/src/plugins/chat/mod.rs old mode 100755 new mode 100644 similarity index 75% rename from azalea-client/src/chat.rs rename to azalea-client/src/plugins/chat/mod.rs index 2bef9570..66c77b56 --- a/azalea-client/src/chat.rs +++ b/azalea-client/src/plugins/chat/mod.rs @@ -1,20 +1,13 @@ //! Implementations of chat-related features. -use std::{ - sync::Arc, - time::{SystemTime, UNIX_EPOCH}, -}; +pub mod handler; + +use std::sync::Arc; 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 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::{ @@ -23,12 +16,28 @@ use bevy_ecs::{ prelude::Event, schedule::IntoSystemConfigs, }; +use handler::{SendChatKindEvent, handle_send_chat_kind_event}; use uuid::Uuid; -use crate::{ - client::Client, - packet_handling::game::{SendPacketEvent, handle_send_packet_event}, -}; +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)] @@ -183,23 +192,6 @@ impl 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_send_packet_event), - ) - .chain(), - ); - } -} - /// A client received a chat message packet. #[derive(Event, Debug, Clone)] pub struct ChatReceivedEvent { @@ -235,63 +227,12 @@ pub fn handle_send_chat_event( } } -/// 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 { diff --git a/azalea-client/src/chunks.rs b/azalea-client/src/plugins/chunks.rs similarity index 95% rename from azalea-client/src/chunks.rs rename to azalea-client/src/plugins/chunks.rs index 67313757..cdda3eba 100644 --- a/azalea-client/src/chunks.rs +++ b/azalea-client/src/plugins/chunks.rs @@ -18,16 +18,14 @@ 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_handling::game::{SendPacketEvent, handle_send_packet_event}, - respawn::perform_respawn, + InstanceHolder, interact::handle_block_interact_event, inventory::InventorySet, + packet::game::SendPacketEvent, respawn::perform_respawn, }; -pub struct ChunkPlugin; -impl Plugin for ChunkPlugin { +pub struct ChunksPlugin; +impl Plugin for ChunksPlugin { fn build(&self, app: &mut App) { app.add_systems( Update, @@ -37,7 +35,7 @@ impl Plugin for ChunkPlugin { handle_chunk_batch_finished_event, ) .chain() - .before(handle_send_packet_event) + .before(handle_outgoing_packets) .before(InventorySet) .before(handle_block_interact_event) .before(perform_respawn), diff --git a/azalea-client/src/disconnect.rs b/azalea-client/src/plugins/disconnect.rs similarity index 100% rename from azalea-client/src/disconnect.rs rename to azalea-client/src/plugins/disconnect.rs diff --git a/azalea-client/src/events.rs b/azalea-client/src/plugins/events.rs similarity index 80% rename from azalea-client/src/events.rs rename to azalea-client/src/plugins/events.rs index aed16bcb..3d34d75f 100644 --- a/azalea-client/src/events.rs +++ b/azalea-client/src/plugins/events.rs @@ -5,6 +5,7 @@ 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, }; @@ -24,28 +25,33 @@ use crate::{ PlayerInfo, chat::{ChatPacket, ChatReceivedEvent}, disconnect::DisconnectEvent, - packet_handling::game::{ - AddPlayerEvent, DeathEvent, KeepAliveEvent, PacketEvent, RemovePlayerEvent, + packet::game::{ + AddPlayerEvent, DeathEvent, KeepAliveEvent, ReceivePacketEvent, 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") +// - 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. // -// - 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` +// 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. @@ -111,8 +117,8 @@ pub enum Event { #[derive(Component, Deref, DerefMut)] pub struct LocalPlayerEvents(pub mpsc::UnboundedSender); -pub struct EventPlugin; -impl Plugin for EventPlugin { +pub struct EventsPlugin; +impl Plugin for EventsPlugin { fn build(&self, app: &mut App) { app.add_systems( Update, @@ -130,7 +136,7 @@ impl Plugin for EventPlugin { ) .add_systems( PreUpdate, - init_listener.before(crate::packet_handling::game::process_packet_events), + init_listener.before(crate::packet::game::process_packet_events), ) .add_systems(GameTick, tick_listener); } @@ -166,7 +172,10 @@ pub fn tick_listener(query: Query<&LocalPlayerEvents, With>) { } } -pub fn packet_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader) { +pub fn packet_listener( + query: Query<&LocalPlayerEvents>, + mut events: EventReader, +) { for event in events.read() { let local_player_events = query .get(event.entity) @@ -219,6 +228,13 @@ pub fn death_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader< } } +/// 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, diff --git a/azalea-client/src/interact.rs b/azalea-client/src/plugins/interact.rs similarity index 98% rename from azalea-client/src/interact.rs rename to azalea-client/src/plugins/interact.rs index fdeff197..1a344cc8 100644 --- a/azalea-client/src/interact.rs +++ b/azalea-client/src/plugins/interact.rs @@ -30,13 +30,14 @@ use bevy_ecs::{ 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_handling::game::{SendPacketEvent, handle_send_packet_event}, + packet::game::SendPacketEvent, respawn::perform_respawn, }; @@ -54,7 +55,7 @@ impl Plugin for InteractPlugin { handle_block_interact_event, handle_swing_arm_event, ) - .before(handle_send_packet_event) + .before(handle_outgoing_packets) .after(InventorySet) .after(perform_respawn) .after(handle_attack_event) diff --git a/azalea-client/src/inventory.rs b/azalea-client/src/plugins/inventory.rs similarity index 99% rename from azalea-client/src/inventory.rs rename to azalea-client/src/plugins/inventory.rs index 4d796c9c..3f823ca2 100644 --- a/azalea-client/src/inventory.rs +++ b/azalea-client/src/plugins/inventory.rs @@ -25,11 +25,9 @@ use bevy_ecs::{ }; use tracing::warn; +use super::packet::game::handle_outgoing_packets; use crate::{ - Client, - local_player::PlayerAbilities, - packet_handling::game::{SendPacketEvent, handle_send_packet_event}, - respawn::perform_respawn, + Client, local_player::PlayerAbilities, packet::game::SendPacketEvent, respawn::perform_respawn, }; pub struct InventoryPlugin; @@ -48,7 +46,7 @@ impl Plugin for InventoryPlugin { handle_menu_opened_event, handle_set_container_content_event, handle_container_click_event, - handle_container_close_event.before(handle_send_packet_event), + handle_container_close_event.before(handle_outgoing_packets), handle_client_side_close_container_event, ) .chain() diff --git a/azalea-client/src/mining.rs b/azalea-client/src/plugins/mining.rs similarity index 99% rename from azalea-client/src/mining.rs rename to azalea-client/src/plugins/mining.rs index 03063b3e..beb380b7 100644 --- a/azalea-client/src/mining.rs +++ b/azalea-client/src/plugins/mining.rs @@ -18,12 +18,12 @@ use crate::{ inventory::{Inventory, InventorySet}, local_player::{LocalGameMode, PermissionLevel, PlayerAbilities}, movement::MoveEventsSet, - packet_handling::game::SendPacketEvent, + packet::game::SendPacketEvent, }; /// A plugin that allows clients to break blocks in the world. -pub struct MinePlugin; -impl Plugin for MinePlugin { +pub struct MiningPlugin; +impl Plugin for MiningPlugin { fn build(&self, app: &mut App) { app.add_event::() .add_event::() @@ -59,6 +59,7 @@ impl Plugin for MinePlugin { } } +/// The Bevy system set for things related to mining. #[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)] pub struct MiningSet; 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/movement.rs b/azalea-client/src/plugins/movement.rs similarity index 99% rename from azalea-client/src/movement.rs rename to azalea-client/src/plugins/movement.rs index b0ff70f4..17b92e65 100644 --- a/azalea-client/src/movement.rs +++ b/azalea-client/src/plugins/movement.rs @@ -27,7 +27,7 @@ use bevy_ecs::{ use thiserror::Error; use crate::client::Client; -use crate::packet_handling::game::SendPacketEvent; +use crate::packet::game::SendPacketEvent; #[derive(Error, Debug)] pub enum MovePlayerError { @@ -47,9 +47,9 @@ impl From for MovePlayerError { } } -pub struct PlayerMovePlugin; +pub struct MovementPlugin; -impl Plugin for PlayerMovePlugin { +impl Plugin for MovementPlugin { fn build(&self, app: &mut App) { app.add_event::() .add_event::() 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/packet_handling/login.rs b/azalea-client/src/plugins/packet/login.rs similarity index 98% rename from azalea-client/src/packet_handling/login.rs rename to azalea-client/src/plugins/packet/login.rs index 8cf45afc..1bb07266 100644 --- a/azalea-client/src/packet_handling/login.rs +++ b/azalea-client/src/plugins/packet/login.rs @@ -20,7 +20,7 @@ use tracing::error; /// 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 +/// `.before(azalea::packet::login::process_packet_events)` to it /// because that system clears the events. #[derive(Event, Debug, Clone)] pub struct LoginPacketEvent { diff --git a/azalea-client/src/packet_handling/mod.rs b/azalea-client/src/plugins/packet/mod.rs similarity index 61% rename from azalea-client/src/packet_handling/mod.rs rename to azalea-client/src/plugins/packet/mod.rs index 908f368e..cbd8a175 100644 --- a/azalea-client/src/packet_handling/mod.rs +++ b/azalea-client/src/plugins/packet/mod.rs @@ -1,6 +1,9 @@ use azalea_entity::{EntityUpdateSet, metadata::Health}; use bevy_app::{App, First, Plugin, PreUpdate, Update}; -use bevy_ecs::prelude::*; +use bevy_ecs::{ + prelude::*, + system::{SystemParam, SystemState}, +}; use self::{ game::{ @@ -11,11 +14,11 @@ use self::{ }; use crate::{chat::ChatReceivedEvent, events::death_listener}; -pub mod configuration; +pub mod config; pub mod game; pub mod login; -pub struct PacketHandlerPlugin; +pub struct PacketPlugin; pub fn death_event_on_0_health( query: Query<(Entity, &Health), Changed>, @@ -31,11 +34,11 @@ pub fn death_event_on_0_health( } } -impl Plugin for PacketHandlerPlugin { +impl Plugin for PacketPlugin { fn build(&self, app: &mut App) { app.add_systems( First, - (game::send_packet_events, configuration::send_packet_events), + (game::send_receivepacketevent, config::send_packet_events), ) .add_systems( PreUpdate, @@ -43,7 +46,7 @@ impl Plugin for PacketHandlerPlugin { game::process_packet_events // we want to index and deindex right after .before(EntityUpdateSet::Deindex), - configuration::process_packet_events, + config::process_packet_events, login::handle_send_packet_event, login::process_packet_events, ), @@ -52,18 +55,18 @@ impl Plugin for PacketHandlerPlugin { Update, ( ( - configuration::handle_send_packet_event, - game::handle_send_packet_event, + 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::>() + .init_resource::>() + .init_resource::>() .add_event::() - .add_event::() + .add_event::() .add_event::() .add_event::() .add_event::() @@ -76,3 +79,31 @@ impl Plugin for PacketHandlerPlugin { .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/respawn.rs b/azalea-client/src/plugins/respawn.rs similarity index 83% rename from azalea-client/src/respawn.rs rename to azalea-client/src/plugins/respawn.rs index 41d1c470..5797406b 100644 --- a/azalea-client/src/respawn.rs +++ b/azalea-client/src/plugins/respawn.rs @@ -2,7 +2,8 @@ use azalea_protocol::packets::game::s_client_command::{self, ServerboundClientCo use bevy_app::{App, Plugin, Update}; use bevy_ecs::prelude::*; -use crate::packet_handling::game::{SendPacketEvent, handle_send_packet_event}; +use super::packet::game::handle_outgoing_packets; +use crate::packet::game::SendPacketEvent; /// Tell the server that we're respawning. #[derive(Event, Debug, Clone)] @@ -15,7 +16,7 @@ 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)); + .add_systems(Update, perform_respawn.before(handle_outgoing_packets)); } } diff --git a/azalea-client/src/task_pool.rs b/azalea-client/src/plugins/task_pool.rs similarity index 100% rename from azalea-client/src/task_pool.rs rename to azalea-client/src/plugins/task_pool.rs diff --git a/azalea-client/src/send_client_end.rs b/azalea-client/src/plugins/tick_end.rs similarity index 93% rename from azalea-client/src/send_client_end.rs rename to azalea-client/src/plugins/tick_end.rs index cb3d5e74..c7737eb1 100644 --- a/azalea-client/src/send_client_end.rs +++ b/azalea-client/src/plugins/tick_end.rs @@ -8,7 +8,7 @@ use azalea_world::InstanceName; use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; -use crate::{mining::MiningSet, packet_handling::game::SendPacketEvent}; +use crate::{mining::MiningSet, packet::game::SendPacketEvent}; /// A plugin that makes clients send a [`ServerboundClientTickEnd`] packet every /// tick. 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