From 7b3e2e4bf793466a351510c7fbbd08234e93bb0e Mon Sep 17 00:00:00 2001 From: mat <27899617+mat-1@users.noreply.github.com> Date: Thu, 21 Sep 2023 11:16:29 -0500 Subject: 1.20.2 (#99) * add configuration state * start updating to 23w31a * implement a bit more of 23w31a * chunk batching * start adding configuration state * ioasfhjgsd * almost works * configuration state mostly implemented * handle other packets in configuration state and fix keepalive * cleanup, fix warnings * 23w32a * fix some doctests * 23w33a * 23w35a * 1.20.2-pre2 * fix system conflicts * 1.20.2-pre4 * make tests compile * tests pass * 1.20.2-rc2 * 1.20.2 * Revert "1.20.2" This reverts commit dd152fd265332ead333c919e585ded6d609d7468. * didn't mean to commit that code --------- Co-authored-by: mat --- azalea-client/src/chunk_batching.rs | 146 +++ azalea-client/src/client.rs | 269 ++-- azalea-client/src/disconnect.rs | 24 +- azalea-client/src/events.rs | 18 +- azalea-client/src/interact.rs | 31 +- azalea-client/src/inventory.rs | 33 +- azalea-client/src/lib.rs | 9 +- azalea-client/src/local_player.rs | 133 +- azalea-client/src/mining.rs | 3 +- azalea-client/src/movement.rs | 77 +- azalea-client/src/packet_handling.rs | 1349 -------------------- azalea-client/src/packet_handling/configuration.rs | 204 +++ azalea-client/src/packet_handling/game.rs | 1295 +++++++++++++++++++ azalea-client/src/packet_handling/mod.rs | 59 + azalea-client/src/ping.rs | 2 +- azalea-client/src/player.rs | 2 +- azalea-client/src/raw_connection.rs | 174 +++ azalea-client/src/received_registries.rs | 33 +- 18 files changed, 2201 insertions(+), 1660 deletions(-) create mode 100644 azalea-client/src/chunk_batching.rs delete mode 100644 azalea-client/src/packet_handling.rs create mode 100644 azalea-client/src/packet_handling/configuration.rs create mode 100644 azalea-client/src/packet_handling/game.rs create mode 100644 azalea-client/src/packet_handling/mod.rs create mode 100644 azalea-client/src/raw_connection.rs (limited to 'azalea-client/src') diff --git a/azalea-client/src/chunk_batching.rs b/azalea-client/src/chunk_batching.rs new file mode 100644 index 00000000..c0e8bb34 --- /dev/null +++ b/azalea-client/src/chunk_batching.rs @@ -0,0 +1,146 @@ +//! Used for Minecraft's chunk batching introduced in 23w31a (1.20.2). It's used +//! for making the server spread out how often it sends us chunk packets +//! depending on our receiving speed. + +use std::time::{Duration, Instant}; + +use azalea_protocol::packets::game::serverbound_chunk_batch_received_packet::ServerboundChunkBatchReceivedPacket; +use bevy_app::{App, Plugin, Update}; +use bevy_ecs::prelude::*; + +use crate::{ + interact::handle_block_interact_event, + inventory::InventorySet, + local_player::{handle_send_packet_event, SendPacketEvent}, + respawn::perform_respawn, +}; + +pub struct ChunkBatchingPlugin; +impl Plugin for ChunkBatchingPlugin { + fn build(&self, app: &mut App) { + app.add_systems( + Update, + ( + handle_chunk_batch_start_event, + handle_chunk_batch_finished_event, + ) + .chain() + .before(handle_send_packet_event) + .before(InventorySet) + .before(handle_block_interact_event) + .before(perform_respawn), + ) + .add_event::() + .add_event::(); + } +} + +#[derive(Component, Clone, Debug)] +pub struct ChunkBatchInfo { + pub start_time: Instant, + pub accumulator: ChunkReceiveSpeedAccumulator, +} + +#[derive(Event)] +pub struct ChunkBatchStartEvent { + pub entity: Entity, +} +#[derive(Event)] +pub struct ChunkBatchFinishedEvent { + pub entity: Entity, + pub batch_size: u32, +} + +pub fn handle_chunk_batch_start_event( + mut query: Query<&mut ChunkBatchInfo>, + mut events: EventReader, +) { + for event in events.iter() { + if let Ok(mut chunk_batch_info) = query.get_mut(event.entity) { + chunk_batch_info.start_time = Instant::now(); + } + } +} + +pub fn handle_chunk_batch_finished_event( + mut query: Query<&mut ChunkBatchInfo>, + mut events: EventReader, + mut send_packets: EventWriter, +) { + for event in events.iter() { + if let Ok(mut chunk_batch_info) = query.get_mut(event.entity) { + let batch_duration = chunk_batch_info.start_time.elapsed(); + if event.batch_size > 0 { + chunk_batch_info + .accumulator + .accumulate(event.batch_size, batch_duration); + } + let millis_per_chunk = + f64::max(0., chunk_batch_info.accumulator.get_millis_per_chunk()); + let desired_chunks_per_tick = if millis_per_chunk == 0. { + // make it the server's problem instead + f32::NAN + } else { + (25. / millis_per_chunk) as f32 + }; + send_packets.send(SendPacketEvent { + entity: event.entity, + packet: ServerboundChunkBatchReceivedPacket { + desired_chunks_per_tick, + } + .get(), + }); + } + } +} + +#[derive(Clone, Debug)] +pub struct ChunkReceiveSpeedAccumulator { + batch_sizes: Vec, + /// as milliseconds + batch_durations: Vec, + index: usize, + filled_size: usize, +} +impl ChunkReceiveSpeedAccumulator { + pub fn new(capacity: usize) -> Self { + Self { + batch_sizes: vec![0; capacity], + batch_durations: vec![0; capacity], + index: 0, + filled_size: 0, + } + } + + pub fn accumulate(&mut self, batch_size: u32, batch_duration: Duration) { + self.batch_sizes[self.index] = batch_size; + self.batch_durations[self.index] = + f32::clamp(batch_duration.as_millis() as f32, 0., 15000.) as u32; + self.index = (self.index + 1) % self.batch_sizes.len(); + if self.filled_size < self.batch_sizes.len() { + self.filled_size += 1; + } + } + + pub fn get_millis_per_chunk(&self) -> f64 { + let mut total_batch_size = 0; + let mut total_batch_duration = 0; + for i in 0..self.filled_size { + total_batch_size += self.batch_sizes[i]; + total_batch_duration += self.batch_durations[i]; + } + if total_batch_size == 0 { + return 0.; + } + total_batch_duration as f64 / total_batch_size as f64 + } +} + +impl Default for ChunkBatchInfo { + fn default() -> Self { + Self { + start_time: Instant::now(), + accumulator: ChunkReceiveSpeedAccumulator::new(50), + } + } +} diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index 8424bf39..cd191e0f 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -1,26 +1,29 @@ use crate::{ attack::{self, AttackPlugin}, chat::ChatPlugin, + chunk_batching::{ChunkBatchInfo, ChunkBatchingPlugin}, disconnect::{DisconnectEvent, DisconnectPlugin}, events::{Event, EventPlugin, LocalPlayerEvents}, interact::{CurrentSequenceNumber, InteractPlugin}, inventory::{InventoryComponent, InventoryPlugin}, local_player::{ - death_event, handle_send_packet_event, GameProfileComponent, Hunger, LocalPlayer, - SendPacketEvent, + death_event, handle_send_packet_event, GameProfileComponent, Hunger, InstanceHolder, + PermissionLevel, PlayerAbilities, SendPacketEvent, TabList, }, mining::{self, MinePlugin}, movement::{LastSentLookDirection, PhysicsState, PlayerMovePlugin}, - packet_handling::{self, PacketHandlerPlugin, PacketReceiver}, + packet_handling::PacketHandlerPlugin, player::retroactively_add_game_profile_component, + raw_connection::RawConnection, respawn::RespawnPlugin, task_pool::TaskPoolPlugin, - Account, PlayerInfo, + Account, PlayerInfo, ReceivedRegistries, }; use azalea_auth::{game_profile::GameProfile, sessionserver::ClientSessionServerError}; +use azalea_buf::McBufWritable; use azalea_chat::FormattedText; -use azalea_core::Vec3; +use azalea_core::{ResourceLocation, Vec3}; use azalea_entity::{ indexing::{EntityIdIndex, Loaded}, metadata::Health, @@ -30,19 +33,21 @@ use azalea_physics::PhysicsPlugin; use azalea_protocol::{ connect::{Connection, ConnectionError}, packets::{ - game::{ - clientbound_player_abilities_packet::ClientboundPlayerAbilitiesPacket, - serverbound_client_information_packet::ServerboundClientInformationPacket, - ClientboundGamePacket, ServerboundGamePacket, + configuration::{ + serverbound_client_information_packet::ClientInformation, + ClientboundConfigurationPacket, ServerboundConfigurationPacket, }, - handshake::{ + game::ServerboundGamePacket, + handshaking::{ client_intention_packet::ClientIntentionPacket, ClientboundHandshakePacket, ServerboundHandshakePacket, }, login::{ - serverbound_custom_query_packet::ServerboundCustomQueryPacket, + serverbound_custom_query_answer_packet::ServerboundCustomQueryAnswerPacket, serverbound_hello_packet::ServerboundHelloPacket, - serverbound_key_packet::ServerboundKeyPacket, ClientboundLoginPacket, + serverbound_key_packet::ServerboundKeyPacket, + serverbound_login_acknowledged_packet::ServerboundLoginAcknowledgedPacket, + ClientboundLoginPacket, }, ConnectionProtocol, PROTOCOL_VERSION, }, @@ -59,10 +64,12 @@ use bevy_ecs::{ world::World, }; use bevy_time::{prelude::FixedTime, TimePlugin}; -use derive_more::{Deref, DerefMut}; +use derive_more::Deref; use log::{debug, error}; use parking_lot::{Mutex, RwLock}; -use std::{collections::HashMap, fmt::Debug, io, net::SocketAddr, sync::Arc, time::Duration}; +use std::{ + collections::HashMap, fmt::Debug, io, net::SocketAddr, ops::Deref, sync::Arc, time::Duration, +}; use thiserror::Error; use tokio::{ sync::{broadcast, mpsc}, @@ -71,7 +78,6 @@ use tokio::{ use uuid::Uuid; /// `Client` has the things that a user interacting with the library will want. -/// Things that a player in the world will want to know are in [`LocalPlayer`]. /// /// To make a new client, use either [`azalea::ClientBuilder`] or /// [`Client::join`]. @@ -105,62 +111,6 @@ pub struct Client { pub run_schedule_sender: mpsc::UnboundedSender<()>, } -/// A component that contains some of the "settings" for this client that are -/// sent to the server, such as render distance. This is only present on local -/// players. -pub type ClientInformation = ServerboundClientInformationPacket; - -/// A component that contains the abilities the player has, like flying -/// or instantly breaking blocks. This is only present on local players. -#[derive(Clone, Debug, Component, Default)] -pub struct PlayerAbilities { - pub invulnerable: bool, - pub flying: bool, - pub can_fly: bool, - /// Whether the player can instantly break blocks and can duplicate blocks - /// in their inventory. - pub instant_break: bool, - - pub flying_speed: f32, - /// Used for the fov - pub walking_speed: f32, -} -impl From for PlayerAbilities { - fn from(packet: ClientboundPlayerAbilitiesPacket) -> Self { - Self { - invulnerable: packet.flags.invulnerable, - flying: packet.flags.flying, - can_fly: packet.flags.can_fly, - instant_break: packet.flags.instant_break, - flying_speed: packet.flying_speed, - walking_speed: packet.walking_speed, - } - } -} - -/// Level must be 0..=4 -#[derive(Component, Clone, Default, Deref, DerefMut)] -pub struct PermissionLevel(pub u8); - -/// A component and resource that contains a map of player UUIDs to their -/// information in the tab list. -/// -/// This is a component on local players in case you want to get the tab list -/// that a certain client is seeing, and it's also a resource in case you know -/// that the server gives the same tab list to every player. -/// -/// ``` -/// # use azalea_client::TabList; -/// # fn example(client: &azalea_client::Client) { -/// let tab_list = client.component::(); -/// println!("Online players:"); -/// for (uuid, player_info) in tab_list.iter() { -/// println!("- {} ({}ms)", player_info.profile.name, player_info.latency); -/// } -/// # } -#[derive(Component, Resource, Clone, Debug, Deref, DerefMut, Default)] -pub struct TabList(HashMap); - /// An error that happened while joining the server. #[derive(Error, Debug)] pub enum JoinError { @@ -261,71 +211,55 @@ impl Client { let entity = ecs_lock.lock().spawn(account.to_owned()).id(); let conn = Connection::new(resolved_address).await?; - let (conn, game_profile) = Self::handshake(conn, account, address).await?; + let (mut conn, game_profile) = Self::handshake(conn, account, address).await?; + + { + // quickly send the brand here + let mut brand_data = Vec::new(); + // they don't have to know :) + "vanilla".write_into(&mut brand_data).unwrap(); + conn.write( + azalea_protocol::packets::configuration::serverbound_custom_payload_packet::ServerboundCustomPayloadPacket { + identifier: ResourceLocation::new("brand"), + data: brand_data.into(), + } + .get(), + ).await?; + } + let (read_conn, write_conn) = conn.into_split(); + let (read_conn, write_conn) = (read_conn.raw, write_conn.raw); // we did the handshake, so now we're connected to the server let (tx, rx) = mpsc::unbounded_channel(); - let (packet_writer_sender, packet_writer_receiver) = mpsc::unbounded_channel(); - - // start receiving packets - let packet_receiver = packet_handling::PacketReceiver { - packets: Arc::new(Mutex::new(Vec::new())), - run_schedule_sender: run_schedule_sender.clone(), - }; - - let read_packets_task = tokio::spawn(packet_receiver.clone().read_task(read_conn)); - let write_packets_task = tokio::spawn( - packet_receiver - .clone() - .write_task(write_conn, packet_writer_receiver), - ); - - let local_player = crate::local_player::LocalPlayer::new( - entity, - packet_writer_sender, - // default to an empty world, it'll be set correctly later when we - // get the login packet - Arc::new(RwLock::new(Instance::default())), - read_packets_task, - write_packets_task, - ); - - ecs_lock - .lock() - .entity_mut(entity) - .insert(JoinedClientBundle { - local_player, - packet_receiver, - game_profile: GameProfileComponent(game_profile.clone()), - physics_state: PhysicsState::default(), - local_player_events: LocalPlayerEvents(tx), - inventory: InventoryComponent::default(), - client_information: ClientInformation::default(), - tab_list: TabList::default(), - current_sequence_number: CurrentSequenceNumber::default(), - last_sent_direction: LastSentLookDirection::default(), - abilities: PlayerAbilities::default(), - permission_level: PermissionLevel::default(), - hunger: Hunger::default(), - - entity_id_index: EntityIdIndex::default(), - - mining: mining::MineBundle::default(), - attack: attack::AttackBundle::default(), - - _local: LocalEntity, - _loaded: Loaded, - }); + let mut ecs = ecs_lock.lock(); + // we got the ConfigurationConnection, so the client is now connected :) let client = Client::new( - game_profile, + game_profile.clone(), entity, ecs_lock.clone(), run_schedule_sender.clone(), ); + + ecs.entity_mut(entity).insert(( + // these stay when we switch to the game state + LocalPlayerBundle { + raw_connection: RawConnection::new( + run_schedule_sender, + ConnectionProtocol::Configuration, + read_conn, + write_conn, + ), + received_registries: ReceivedRegistries::default(), + local_player_events: LocalPlayerEvents(tx), + game_profile: GameProfileComponent(game_profile), + }, + InConfigurationState, + )); + Ok((client, rx)) } @@ -340,7 +274,7 @@ impl Client { address: &ServerAddress, ) -> Result< ( - Connection, + Connection, GameProfile, ), JoinError, @@ -362,7 +296,9 @@ impl Client { conn.write( ServerboundHelloPacket { name: account.username.clone(), - profile_id: account.uuid, + // TODO: pretty sure this should generate an offline-mode uuid instead of just + // Uuid::default() + profile_id: account.uuid.unwrap_or_default(), } .get(), ) @@ -428,8 +364,13 @@ impl Client { conn.set_compression_threshold(p.compression_threshold); } ClientboundLoginPacket::GameProfile(p) => { - debug!("Got profile {:?}", p.game_profile); - break (conn.game(), p.game_profile); + debug!( + "Got profile {:?}. handshake is finished and we're now switching to the configuration state", + p.game_profile + ); + conn.write(ServerboundLoginAcknowledgedPacket {}.get()) + .await?; + break (conn.configuration(), p.game_profile); } ClientboundLoginPacket::LoginDisconnect(p) => { debug!("Got disconnect {:?}", p); @@ -438,7 +379,7 @@ impl Client { ClientboundLoginPacket::CustomQuery(p) => { debug!("Got custom query {:?}", p); conn.write( - ServerboundCustomQueryPacket { + ServerboundCustomQueryAnswerPacket { transaction_id: p.transaction_id, data: None, } @@ -453,9 +394,12 @@ impl Client { } /// Write a packet directly to the server. - pub fn write_packet(&self, packet: ServerboundGamePacket) { - self.local_player_mut(&mut self.ecs.lock()) - .write_packet(packet); + pub fn write_packet( + &self, + packet: ServerboundGamePacket, + ) -> Result<(), crate::raw_connection::WritePacketError> { + self.raw_connection_mut(&mut self.ecs.lock()) + .write_packet(packet) } /// Disconnect this client from the server by ending all tasks. @@ -468,14 +412,24 @@ impl Client { }); } - pub fn local_player<'a>(&'a self, ecs: &'a mut World) -> &'a LocalPlayer { - self.query::<&LocalPlayer>(ecs) + pub fn local_player<'a>(&'a self, ecs: &'a mut World) -> &'a InstanceHolder { + self.query::<&InstanceHolder>(ecs) } pub fn local_player_mut<'a>( &'a self, ecs: &'a mut World, - ) -> bevy_ecs::world::Mut<'a, LocalPlayer> { - self.query::<&mut LocalPlayer>(ecs) + ) -> bevy_ecs::world::Mut<'a, InstanceHolder> { + self.query::<&mut InstanceHolder>(ecs) + } + + pub fn raw_connection<'a>(&'a self, ecs: &'a mut World) -> &'a RawConnection { + self.query::<&RawConnection>(ecs) + } + pub fn raw_connection_mut<'a>( + &'a self, + ecs: &'a mut World, + ) -> bevy_ecs::world::Mut<'a, RawConnection> { + self.query::<&mut RawConnection>(ecs) } /// Get a component from this client. This will clone the component and @@ -538,8 +492,8 @@ impl Client { /// ``` pub async fn set_client_information( &self, - client_information: ServerboundClientInformationPacket, - ) -> Result<(), std::io::Error> { + client_information: ClientInformation, + ) -> Result<(), crate::raw_connection::WritePacketError> { { let mut ecs = self.ecs.lock(); let mut client_information_mut = self.query::<&mut ClientInformation>(&mut ecs); @@ -551,7 +505,7 @@ impl Client { "Sending client information (already logged in): {:?}", client_information ); - self.write_packet(client_information.get()); + self.write_packet(azalea_protocol::packets::game::serverbound_client_information_packet::ServerboundClientInformationPacket { information: client_information.clone() }.get())?; } Ok(()) @@ -606,21 +560,33 @@ impl Client { /// Get a map of player UUIDs to their information in the tab list. /// - /// This is a shortcut for `bot.component::().0`. + /// This is a shortcut for `*bot.component::()`. pub fn tab_list(&self) -> HashMap { - self.component::().0 + self.component::().deref().clone() } } -/// A bundle for the components that are present on a local player that received -/// a login packet. If you want to filter for this, just use [`LocalEntity`]. +/// The bundle of components that's shared when we're either in the +/// `configuration` or `game` state. +/// +/// For the components that are only present in the `game` state, see +/// [`JoinedClientBundle`] and for the ones in the `configuration` state, see +/// [`ConfigurationClientBundle`]. #[derive(Bundle)] -pub struct JoinedClientBundle { - pub local_player: LocalPlayer, - pub packet_receiver: PacketReceiver, +pub struct LocalPlayerBundle { + pub raw_connection: RawConnection, + pub received_registries: ReceivedRegistries, + pub local_player_events: LocalPlayerEvents, pub game_profile: GameProfileComponent, +} + +/// 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)] +pub struct JoinedClientBundle { + pub instance_holder: InstanceHolder, pub physics_state: PhysicsState, - pub local_player_events: LocalPlayerEvents, pub inventory: InventoryComponent, pub client_information: ClientInformation, pub tab_list: TabList, @@ -628,6 +594,7 @@ pub struct JoinedClientBundle { pub last_sent_direction: LastSentLookDirection, pub abilities: PlayerAbilities, pub permission_level: PermissionLevel, + pub chunk_batch_info: ChunkBatchInfo, pub hunger: Hunger, pub entity_id_index: EntityIdIndex, @@ -635,10 +602,15 @@ pub struct JoinedClientBundle { pub mining: mining::MineBundle, pub attack: attack::AttackBundle, - pub _local: LocalEntity, + pub _local_entity: LocalEntity, pub _loaded: Loaded, } +/// A marker component for local players that are currently in the +/// `configuration` state. +#[derive(Component)] +pub struct InConfigurationState; + pub struct AzaleaPlugin; impl Plugin for AzaleaPlugin { fn build(&self, app: &mut App) { @@ -790,6 +762,7 @@ impl PluginGroup for DefaultPlugins { .add(RespawnPlugin) .add(MinePlugin) .add(AttackPlugin) + .add(ChunkBatchingPlugin) .add(TickBroadcastPlugin); #[cfg(feature = "log")] { diff --git a/azalea-client/src/disconnect.rs b/azalea-client/src/disconnect.rs index 10aef7ba..966e5bb7 100644 --- a/azalea-client/src/disconnect.rs +++ b/azalea-client/src/disconnect.rs @@ -12,7 +12,7 @@ use bevy_ecs::{ }; use derive_more::Deref; -use crate::{client::JoinedClientBundle, LocalPlayer}; +use crate::{client::JoinedClientBundle, raw_connection::RawConnection}; pub struct DisconnectPlugin; impl Plugin for DisconnectPlugin { @@ -21,7 +21,7 @@ impl Plugin for DisconnectPlugin { PostUpdate, ( update_read_packets_task_running_component, - disconnect_on_read_packets_ended, + disconnect_on_connection_dead, remove_components_from_disconnected_players, ) .chain(), @@ -47,25 +47,23 @@ pub fn remove_components_from_disconnected_players( } #[derive(Component, Clone, Copy, Debug, Deref)] -pub struct ReadPacketsTaskRunning(bool); +pub struct IsConnectionAlive(bool); fn update_read_packets_task_running_component( + query: Query<(Entity, &RawConnection)>, mut commands: Commands, - local_player: Query<(Entity, &LocalPlayer)>, ) { - for (entity, local_player) in &local_player { - let running = !local_player.read_packets_task.is_finished(); - commands - .entity(entity) - .insert(ReadPacketsTaskRunning(running)); + for (entity, raw_connection) in &query { + let running = raw_connection.is_alive(); + commands.entity(entity).insert(IsConnectionAlive(running)); } } -fn disconnect_on_read_packets_ended( - local_player: Query<(Entity, &ReadPacketsTaskRunning), Changed>, +fn disconnect_on_connection_dead( + query: Query<(Entity, &IsConnectionAlive), Changed>, mut disconnect_events: EventWriter, ) { - for (entity, &read_packets_task_running) in &local_player { - if !*read_packets_task_running { + for (entity, &is_connection_alive) in &query { + if !*is_connection_alive { disconnect_events.send(DisconnectEvent { entity }); } } diff --git a/azalea-client/src/events.rs b/azalea-client/src/events.rs index 0d34d47b..17ebd4e8 100644 --- a/azalea-client/src/events.rs +++ b/azalea-client/src/events.rs @@ -20,7 +20,7 @@ use tokio::sync::mpsc; use crate::{ chat::{ChatPacket, ChatReceivedEvent}, - packet_handling::{ + packet_handling::game::{ AddPlayerEvent, DeathEvent, KeepAliveEvent, PacketEvent, RemovePlayerEvent, UpdatePlayerEvent, }, @@ -115,13 +115,13 @@ impl Plugin for EventPlugin { add_player_listener, update_player_listener, remove_player_listener, - death_listener, keepalive_listener, + death_listener, ), ) .add_systems( PreUpdate, - init_listener.before(crate::packet_handling::process_packet_events), + init_listener.before(crate::packet_handling::game::process_packet_events), ) .add_systems(FixedUpdate, tick_listener); } @@ -145,7 +145,7 @@ fn chat_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader, mut events: EventReader, mut events: EventReader for event in events.iter() { let local_player_events = query .get(event.entity) - .expect("Non-localplayer entities shouldn't be able to receive add player events"); + .expect("Non-local entities shouldn't be able to receive add player events"); local_player_events .send(Event::AddPlayer(event.info.clone())) .unwrap(); @@ -188,7 +188,7 @@ fn update_player_listener( for event in events.iter() { let local_player_events = query .get(event.entity) - .expect("Non-localplayer entities shouldn't be able to receive update player events"); + .expect("Non-local entities shouldn't be able to receive update player events"); local_player_events .send(Event::UpdatePlayer(event.info.clone())) .unwrap(); @@ -202,7 +202,7 @@ fn remove_player_listener( for event in events.iter() { let local_player_events = query .get(event.entity) - .expect("Non-localplayer entities shouldn't be able to receive remove player events"); + .expect("Non-local entities shouldn't be able to receive remove player events"); local_player_events .send(Event::RemovePlayer(event.info.clone())) .unwrap(); @@ -223,7 +223,7 @@ fn keepalive_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader< for event in events.iter() { let local_player_events = query .get(event.entity) - .expect("Non-localplayer entities shouldn't be able to receive keepalive events"); + .expect("Non-local entities shouldn't be able to receive keepalive events"); local_player_events .send(Event::KeepAlive(event.id)) .unwrap(); diff --git a/azalea-client/src/interact.rs b/azalea-client/src/interact.rs index b28baca5..dc1306e3 100644 --- a/azalea-client/src/interact.rs +++ b/azalea-client/src/interact.rs @@ -27,10 +27,13 @@ use derive_more::{Deref, DerefMut}; use log::warn; use crate::{ - client::{PermissionLevel, PlayerAbilities}, + attack::handle_attack_event, inventory::{InventoryComponent, InventorySet}, - local_player::{handle_send_packet_event, LocalGameMode, SendPacketEvent}, - Client, LocalPlayer, + local_player::{ + handle_send_packet_event, LocalGameMode, PermissionLevel, PlayerAbilities, SendPacketEvent, + }, + respawn::perform_respawn, + Client, }; /// A plugin that allows clients to interact with blocks in the world. @@ -48,6 +51,9 @@ impl Plugin for InteractPlugin { handle_swing_arm_event, ) .before(handle_send_packet_event) + .after(InventorySet) + .after(perform_respawn) + .after(handle_attack_event) .chain(), update_modifiers_for_held_item .after(InventorySet) @@ -100,16 +106,12 @@ pub struct HitResultComponent(BlockHitResult); pub fn handle_block_interact_event( mut events: EventReader, - mut query: Query<( - &LocalPlayer, - &mut CurrentSequenceNumber, - &HitResultComponent, - )>, + mut query: Query<(Entity, &mut CurrentSequenceNumber, &HitResultComponent)>, + mut send_packet_events: EventWriter, ) { for event in events.iter() { - let Ok((local_player, mut sequence_number, hit_result)) = query.get_mut(event.entity) - else { - warn!("Sent BlockInteractEvent for entity that isn't LocalPlayer"); + let Ok((entity, mut sequence_number, hit_result)) = query.get_mut(event.entity) else { + warn!("Sent BlockInteractEvent for entity that doesn't have the required components"); continue; }; @@ -141,14 +143,15 @@ pub fn handle_block_interact_event( } }; - local_player.write_packet( - ServerboundUseItemOnPacket { + send_packet_events.send(SendPacketEvent { + entity, + packet: ServerboundUseItemOnPacket { hand: InteractionHand::MainHand, block_hit, sequence: sequence_number.0, } .get(), - ) + }) } } diff --git a/azalea-client/src/inventory.rs b/azalea-client/src/inventory.rs index da5376fc..2e8478d4 100644 --- a/azalea-client/src/inventory.rs +++ b/azalea-client/src/inventory.rs @@ -25,7 +25,11 @@ use bevy_ecs::{ }; use log::warn; -use crate::{client::PlayerAbilities, local_player::handle_send_packet_event, Client, LocalPlayer}; +use crate::{ + local_player::{handle_send_packet_event, PlayerAbilities, SendPacketEvent}, + respawn::perform_respawn, + Client, +}; pub struct InventoryPlugin; impl Plugin for InventoryPlugin { @@ -45,7 +49,8 @@ impl Plugin for InventoryPlugin { handle_client_side_close_container_event, ) .chain() - .in_set(InventorySet), + .in_set(InventorySet) + .before(perform_respawn), ); } } @@ -599,12 +604,13 @@ pub struct CloseContainerEvent { pub id: u8, } fn handle_container_close_event( + query: Query<(Entity, &InventoryComponent)>, mut events: EventReader, mut client_side_events: EventWriter, - query: Query<(&LocalPlayer, &InventoryComponent)>, + mut send_packet_events: EventWriter, ) { for event in events.iter() { - let (local_player, inventory) = query.get(event.entity).unwrap(); + let (entity, inventory) = query.get(event.entity).unwrap(); if event.id != inventory.id { warn!( "Tried to close container with ID {}, but the current container ID is {}", @@ -613,12 +619,13 @@ fn handle_container_close_event( continue; } - local_player.write_packet( - ServerboundContainerClosePacket { + send_packet_events.send(SendPacketEvent { + entity, + packet: ServerboundContainerClosePacket { container_id: inventory.id, } .get(), - ); + }); client_side_events.send(ClientSideCloseContainerEvent { entity: event.entity, }); @@ -650,11 +657,12 @@ pub struct ContainerClickEvent { pub operation: ClickOperation, } pub fn handle_container_click_event( + mut query: Query<(Entity, &mut InventoryComponent)>, mut events: EventReader, - mut query: Query<(&mut InventoryComponent, &LocalPlayer)>, + mut send_packet_events: EventWriter, ) { for event in events.iter() { - let (mut inventory, local_player) = query.get_mut(event.entity).unwrap(); + let (entity, mut inventory) = query.get_mut(event.entity).unwrap(); if inventory.id != event.window_id { warn!( "Tried to click container with ID {}, but the current container ID is {}", @@ -678,8 +686,9 @@ pub fn handle_container_click_event( } } - local_player.write_packet( - ServerboundContainerClickPacket { + send_packet_events.send(SendPacketEvent { + entity, + packet: ServerboundContainerClickPacket { container_id: event.window_id, state_id: inventory.state_id, slot_num: event.operation.slot_num().map(|n| n as i16).unwrap_or(-999), @@ -689,7 +698,7 @@ pub fn handle_container_click_event( carried_item: inventory.carried.clone(), } .get(), - ) + }) } } diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs index 40544540..25c75bcf 100644 --- a/azalea-client/src/lib.rs +++ b/azalea-client/src/lib.rs @@ -13,6 +13,7 @@ mod account; pub mod attack; pub mod chat; +pub mod chunk_batching; mod client; pub mod disconnect; mod entity_query; @@ -26,18 +27,20 @@ pub mod movement; pub mod packet_handling; pub mod ping; mod player; +pub mod raw_connection; pub mod received_registries; pub mod respawn; pub mod task_pool; pub use account::{Account, AccountOpts}; +pub use azalea_protocol::packets::configuration::serverbound_client_information_packet::ClientInformation; pub use client::{ - start_ecs_runner, Client, ClientInformation, DefaultPlugins, JoinError, JoinedClientBundle, - TabList, TickBroadcast, + start_ecs_runner, Client, DefaultPlugins, JoinError, JoinedClientBundle, TickBroadcast, }; pub use events::Event; -pub use local_player::{GameProfileComponent, LocalPlayer, SendPacketEvent}; +pub use local_player::{GameProfileComponent, InstanceHolder, SendPacketEvent, TabList}; pub use movement::{ PhysicsState, SprintDirection, StartSprintEvent, StartWalkEvent, WalkDirection, }; pub use player::PlayerInfo; +pub use received_registries::ReceivedRegistries; diff --git a/azalea-client/src/local_player.rs b/azalea-client/src/local_player.rs index 1bcb0948..2989f36e 100644 --- a/azalea-client/src/local_player.rs +++ b/azalea-client/src/local_player.rs @@ -1,51 +1,40 @@ -use std::{io, sync::Arc}; +use std::{collections::HashMap, io, sync::Arc}; use azalea_auth::game_profile::GameProfile; use azalea_core::GameMode; use azalea_entity::Dead; -use azalea_protocol::packets::game::ServerboundGamePacket; +use azalea_protocol::packets::game::{ + clientbound_player_abilities_packet::ClientboundPlayerAbilitiesPacket, ServerboundGamePacket, +}; use azalea_world::{Instance, PartialInstance}; use bevy_ecs::{ - component::Component, entity::Entity, event::EventReader, prelude::Event, query::Added, + component::Component, entity::Entity, event::EventReader, prelude::*, query::Added, system::Query, }; use derive_more::{Deref, DerefMut}; +use log::error; use parking_lot::RwLock; use thiserror::Error; -use tokio::{sync::mpsc, task::JoinHandle}; +use tokio::sync::mpsc; +use uuid::Uuid; use crate::{ events::{Event as AzaleaEvent, LocalPlayerEvents}, - ClientInformation, + raw_connection::RawConnection, + ClientInformation, PlayerInfo, }; -/// This is a component for our local player entities that are probably in a -/// world. If you have access to a [`Client`], you probably don't need to care -/// about this since `Client` gives you access to everything here. -/// -/// You can also use the [`LocalEntity`] marker component for queries if you're -/// only checking for a local player and don't need the contents of this -/// component. -/// -/// [`LocalEntity`]: azalea_entity::LocalEntity -/// [`Client`]: crate::Client +/// A component that keeps strong references to our [`PartialInstance`] and +/// [`Instance`] for local players. #[derive(Component)] -pub struct LocalPlayer { - pub packet_writer: mpsc::UnboundedSender, - +pub struct InstanceHolder { /// The partial instance is the world this client currently has loaded. It /// has a limited render distance. pub partial_instance: Arc>, /// The world is the combined [`PartialInstance`]s of all clients in the /// same world. (Only relevant if you're using a shared world, i.e. a /// swarm) - pub world: Arc>, - - /// A task that reads packets from the server. The client is disconnected - /// when this task ends. - pub(crate) read_packets_task: JoinHandle<()>, - /// A task that writes packets from the server. - pub(crate) write_packets_task: JoinHandle<()>, + pub instance: Arc>, } /// A component only present in players that contains the [`GameProfile`] (which @@ -64,6 +53,53 @@ pub struct LocalGameMode { pub previous: Option, } +/// A component that contains the abilities the player has, like flying +/// or instantly breaking blocks. This is only present on local players. +#[derive(Clone, Debug, Component, Default)] +pub struct PlayerAbilities { + pub invulnerable: bool, + pub flying: bool, + pub can_fly: bool, + /// Whether the player can instantly break blocks and can duplicate blocks + /// in their inventory. + pub instant_break: bool, + + pub flying_speed: f32, + /// Used for the fov + pub walking_speed: f32, +} +impl From for PlayerAbilities { + fn from(packet: ClientboundPlayerAbilitiesPacket) -> Self { + Self { + invulnerable: packet.flags.invulnerable, + flying: packet.flags.flying, + can_fly: packet.flags.can_fly, + instant_break: packet.flags.instant_break, + flying_speed: packet.flying_speed, + walking_speed: packet.walking_speed, + } + } +} + +/// Level must be 0..=4 +#[derive(Component, Clone, Default, Deref, DerefMut)] +pub struct PermissionLevel(pub u8); + +/// A component that contains a map of player UUIDs to their information in the +/// tab list. +/// +/// ``` +/// # use azalea_client::TabList; +/// # fn example(client: &azalea_client::Client) { +/// let tab_list = client.component::(); +/// println!("Online players:"); +/// for (uuid, player_info) in tab_list.iter() { +/// println!("- {} ({}ms)", player_info.profile.name, player_info.latency); +/// } +/// # } +#[derive(Component, Resource, Clone, Debug, Deref, DerefMut, Default)] +pub struct TabList(HashMap); + #[derive(Component, Clone)] pub struct Hunger { /// The main hunger bar. Goes from 0 to 20. @@ -83,50 +119,24 @@ impl Default for Hunger { } } -impl LocalPlayer { - /// Create a new `LocalPlayer`. - pub fn new( - entity: Entity, - packet_writer: mpsc::UnboundedSender, - world: Arc>, - read_packets_task: JoinHandle<()>, - write_packets_task: JoinHandle<()>, - ) -> Self { +impl InstanceHolder { + /// Create a new `InstanceHolder`. + pub fn new(entity: Entity, world: Arc>) -> Self { let client_information = ClientInformation::default(); - LocalPlayer { - packet_writer, - - world, + InstanceHolder { + instance: world, partial_instance: Arc::new(RwLock::new(PartialInstance::new( azalea_world::calculate_chunk_storage_range( client_information.view_distance.into(), ), Some(entity), ))), - - read_packets_task, - write_packets_task, } } - - /// Write a packet directly to the server. - pub fn write_packet(&self, packet: ServerboundGamePacket) { - self.packet_writer - .send(packet) - .expect("write_packet shouldn't be able to be called if the connection is closed"); - } -} - -impl Drop for LocalPlayer { - /// Stop every active task when the `LocalPlayer` is dropped. - fn drop(&mut self) { - self.read_packets_task.abort(); - self.write_packets_task.abort(); - } } -/// Send the "Death" event for [`LocalPlayer`]s that died with no reason. +/// 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(); @@ -160,11 +170,14 @@ pub struct SendPacketEvent { pub fn handle_send_packet_event( mut send_packet_events: EventReader, - mut query: Query<&mut LocalPlayer>, + mut query: Query<&mut RawConnection>, ) { for event in send_packet_events.iter() { - if let Ok(local_player) = query.get_mut(event.entity) { - local_player.write_packet(event.packet.clone()); + if let Ok(raw_connection) = query.get_mut(event.entity) { + // 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/mining.rs b/azalea-client/src/mining.rs index 336bfe24..5db357b8 100644 --- a/azalea-client/src/mining.rs +++ b/azalea-client/src/mining.rs @@ -12,13 +12,12 @@ use bevy_ecs::prelude::*; use derive_more::{Deref, DerefMut}; use crate::{ - client::{PermissionLevel, PlayerAbilities}, interact::{ can_use_game_master_blocks, check_is_interaction_restricted, CurrentSequenceNumber, HitResultComponent, SwingArmEvent, }, inventory::{InventoryComponent, InventorySet}, - local_player::{LocalGameMode, SendPacketEvent}, + local_player::{LocalGameMode, PermissionLevel, PlayerAbilities, SendPacketEvent}, Client, }; diff --git a/azalea-client/src/movement.rs b/azalea-client/src/movement.rs index 3bdcdeac..782e98ff 100644 --- a/azalea-client/src/movement.rs +++ b/azalea-client/src/movement.rs @@ -1,8 +1,8 @@ use crate::client::Client; -use crate::local_player::LocalPlayer; +use crate::local_player::SendPacketEvent; use azalea_entity::{metadata::Sprinting, Attributes, Jumping}; use azalea_entity::{InLoadedChunk, LastSentPosition, LookDirection, Physics, Position}; -use azalea_physics::PhysicsSet; +use azalea_physics::{ai_step, PhysicsSet}; use azalea_protocol::packets::game::serverbound_player_command_packet::ServerboundPlayerCommandPacket; use azalea_protocol::packets::game::{ serverbound_move_player_pos_packet::ServerboundMovePlayerPosPacket, @@ -12,7 +12,7 @@ use azalea_protocol::packets::game::{ }; use azalea_world::{MinecraftEntityId, MoveEntityError}; use bevy_app::{App, FixedUpdate, Plugin, Update}; -use bevy_ecs::prelude::Event; +use bevy_ecs::prelude::{Event, EventWriter}; use bevy_ecs::{ component::Component, entity::Entity, event::EventReader, query::With, schedule::IntoSystemConfigs, system::Query, @@ -48,9 +48,11 @@ impl Plugin for PlayerMovePlugin { .add_systems( FixedUpdate, ( - local_player_ai_step + (tick_controls, local_player_ai_step) + .chain() .in_set(PhysicsSet) - .before(azalea_physics::ai_step), + .before(ai_step), + send_sprinting_if_needed.after(azalea_entity::update_in_loaded_chunk), send_position.after(PhysicsSet), ) .chain(), @@ -118,33 +120,28 @@ pub struct PhysicsState { pub fn send_position( mut query: Query< ( - &MinecraftEntityId, - &mut LocalPlayer, - &mut PhysicsState, + Entity, &Position, + &LookDirection, + &mut PhysicsState, &mut LastSentPosition, &mut Physics, - &LookDirection, &mut LastSentLookDirection, - &Sprinting, ), With, >, + mut send_packet_events: EventWriter, ) { for ( - id, - mut local_player, - mut physics_state, + entity, position, + direction, + mut physics_state, mut last_sent_position, mut physics, - direction, mut last_direction, - sprinting, ) in query.iter_mut() { - local_player.send_sprinting_if_needed(id, sprinting, &mut physics_state); - let packet = { // TODO: the camera being able to be controlled by other entities isn't // implemented yet if !self.is_controlled_camera() { return }; @@ -225,18 +222,16 @@ pub fn send_position( }; if let Some(packet) = packet { - local_player.write_packet(packet); + send_packet_events.send(SendPacketEvent { entity, packet }); } } } -impl LocalPlayer { - fn send_sprinting_if_needed( - &mut self, - id: &MinecraftEntityId, - sprinting: &Sprinting, - physics_state: &mut PhysicsState, - ) { +fn send_sprinting_if_needed( + mut query: Query<(Entity, &MinecraftEntityId, &Sprinting, &mut PhysicsState)>, + mut send_packet_events: EventWriter, +) { + for (entity, minecraft_entity_id, sprinting, mut physics_state) in query.iter_mut() { let was_sprinting = physics_state.was_sprinting; if **sprinting != was_sprinting { let sprinting_action = if **sprinting { @@ -244,21 +239,26 @@ impl LocalPlayer { } else { azalea_protocol::packets::game::serverbound_player_command_packet::Action::StopSprinting }; - self.write_packet( - ServerboundPlayerCommandPacket { - id: **id, + send_packet_events.send(SendPacketEvent { + entity, + packet: ServerboundPlayerCommandPacket { + id: **minecraft_entity_id, action: sprinting_action, data: 0, } .get(), - ); + }); physics_state.was_sprinting = **sprinting; } } +} + +/// Update the impulse from self.move_direction. The multipler is used for +/// sneaking. +pub(crate) fn tick_controls(mut query: Query<&mut PhysicsState>) { + for mut physics_state in query.iter_mut() { + let multiplier: Option = None; - /// Update the impulse from self.move_direction. The multipler is used for - /// sneaking. - pub(crate) fn tick_controls(multiplier: Option, physics_state: &mut PhysicsState) { let mut forward_impulse: f32 = 0.; let mut left_impulse: f32 = 0.; let move_direction = physics_state.move_direction; @@ -296,18 +296,11 @@ impl LocalPlayer { /// automatically by the client. pub fn local_player_ai_step( mut query: Query< - ( - &mut PhysicsState, - &mut Physics, - &mut Sprinting, - &mut Attributes, - ), + (&PhysicsState, &mut Physics, &mut Sprinting, &mut Attributes), With, >, ) { - for (mut physics_state, mut physics, mut sprinting, mut attributes) in query.iter_mut() { - LocalPlayer::tick_controls(None, &mut physics_state); - + for (physics_state, mut physics, mut sprinting, mut attributes) in query.iter_mut() { // server ai step physics.xxa = physics_state.left_impulse; physics.zza = physics_state.forward_impulse; @@ -325,7 +318,7 @@ pub fn local_player_ai_step( && ( // !self.is_in_water() // || self.is_underwater() && - has_enough_impulse_to_start_sprinting(&physics_state) + has_enough_impulse_to_start_sprinting(physics_state) && has_enough_food_to_sprint // && !self.using_item() // && !self.has_effect(MobEffects.BLINDNESS) diff --git a/azalea-client/src/packet_handling.rs b/azalea-client/src/packet_handling.rs deleted file mode 100644 index 6ac657d7..00000000 --- a/azalea-client/src/packet_handling.rs +++ /dev/null @@ -1,1349 +0,0 @@ -use std::{ - collections::HashSet, - io::Cursor, - sync::{Arc, Weak}, -}; - -use azalea_buf::McBufWritable; -use azalea_chat::FormattedText; -use azalea_core::{ChunkPos, GameMode, ResourceLocation, Vec3}; -use azalea_entity::{ - indexing::{EntityIdIndex, EntityUuidIndex}, - metadata::{apply_metadata, Health, PlayerMetadataBundle}, - Dead, EntityBundle, EntityKind, EntityUpdateSet, LastSentPosition, LoadedBy, LookDirection, - Physics, PlayerBundle, Position, RelativeEntityUpdate, -}; -use azalea_nbt::NbtCompound; -use azalea_protocol::{ - connect::{ReadConnection, WriteConnection}, - packets::game::{ - clientbound_player_combat_kill_packet::ClientboundPlayerCombatKillPacket, - serverbound_accept_teleportation_packet::ServerboundAcceptTeleportationPacket, - serverbound_custom_payload_packet::ServerboundCustomPayloadPacket, - serverbound_keep_alive_packet::ServerboundKeepAlivePacket, - serverbound_move_player_pos_rot_packet::ServerboundMovePlayerPosRotPacket, - serverbound_pong_packet::ServerboundPongPacket, ClientboundGamePacket, - ServerboundGamePacket, - }, - read::ReadPacketError, -}; -use azalea_world::{Instance, InstanceContainer, InstanceName, MinecraftEntityId, PartialInstance}; -use bevy_app::{App, First, Plugin, PreUpdate, Update}; -use bevy_ecs::{ - component::Component, - entity::Entity, - event::{EventReader, EventWriter, Events}, - prelude::Event, - query::Changed, - schedule::IntoSystemConfigs, - system::{Commands, Query, Res, ResMut, SystemState}, - world::World, -}; -use log::{debug, error, trace, warn}; -use parking_lot::{Mutex, RwLock}; -use tokio::sync::mpsc; - -use crate::{ - chat::{ChatPacket, ChatReceivedEvent}, - client::{PlayerAbilities, TabList}, - disconnect::DisconnectEvent, - events::death_listener, - inventory::{ - ClientSideCloseContainerEvent, InventoryComponent, MenuOpenedEvent, - SetContainerContentEvent, - }, - local_player::{GameProfileComponent, Hunger, LocalGameMode, LocalPlayer}, - received_registries::ReceivedRegistries, - ClientInformation, PlayerInfo, -}; - -/// An event that's sent when we receive a packet. -/// ``` -/// # use azalea_client::packet_handling::PacketEvent; -/// # use azalea_protocol::packets::game::ClientboundGamePacket; -/// # use bevy_ecs::event::EventReader; -/// -/// fn handle_packets(mut events: EventReader) { -/// for PacketEvent { -/// entity, -/// packet, -/// } in events.iter() { -/// match packet { -/// 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: ClientboundGamePacket, -} - -pub struct PacketHandlerPlugin; - -impl Plugin for PacketHandlerPlugin { - fn build(&self, app: &mut App) { - app.add_systems(First, send_packet_events) - .add_systems( - PreUpdate, - process_packet_events - // we want to index and deindex right after - .before(EntityUpdateSet::Deindex), - ) - .add_systems(Update, death_event_on_0_health.before(death_listener)) - .init_resource::>() - .add_event::() - .add_event::() - .add_event::() - .add_event::() - .add_event::() - .add_event::() - .add_event::() - .add_event::(); - } -} - -/// 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 [`ClientboundPlayerCombatKillPacket`] will -/// be included. -#[derive(Event, Debug, Clone)] -pub struct DeathEvent { - pub entity: Entity, - pub packet: Option, -} - -pub fn death_event_on_0_health( - query: Query<(Entity, &Health), Changed>, - mut death_events: EventWriter, -) { - for (entity, health) in query.iter() { - if **health == 0. { - death_events.send(DeathEvent { - entity, - packet: None, - }); - } - } -} - -/// 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, - 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>, -} - -/// Something that receives packets from the server. -#[derive(Event, Component, Clone)] -pub struct PacketReceiver { - pub packets: Arc>>, - pub run_schedule_sender: mpsc::UnboundedSender<()>, -} - -pub fn send_packet_events( - query: Query<(Entity, &PacketReceiver)>, - 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, packet_receiver) in &query { - let mut packets = packet_receiver.packets.lock(); - if !packets.is_empty() { - for packet in packets.iter() { - packet_events.send(PacketEvent { - entity: player_entity, - packet: packet.clone(), - }); - } - // 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 PacketEvent { - entity: player_entity, - packet, - } in events.iter() - { - // 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 { - ClientboundGamePacket::Login(p) => { - debug!("Got login packet"); - - #[allow(clippy::type_complexity)] - let mut system_state: SystemState<( - Commands, - Query<( - &mut LocalPlayer, - &mut EntityIdIndex, - &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 local_player, mut entity_id_index, game_profile, client_information) = - query.get_mut(player_entity).unwrap(); - - { - let received_registries = ReceivedRegistries(p.registry_holder.root); - - let dimension = &received_registries - .dimension_type - .value - .iter() - .find(|t| t.name == p.dimension_type) - .unwrap_or_else(|| { - panic!("No dimension_type with name {}", p.dimension_type) - }) - .element; - - let new_instance_name = p.dimension.clone(); - - // add this world to the instance_container (or don't if it's already - // there) - let instance = instance_container.insert( - new_instance_name.clone(), - dimension.height, - dimension.min_y, - ); - instance_loaded_events.send(InstanceLoadedEvent { - entity: player_entity, - name: new_instance_name.clone(), - instance: Arc::downgrade(&instance), - }); - - // set the partial_world to an empty world - // (when we add chunks or entities those will be in the - // instance_container) - - *local_player.partial_instance.write() = PartialInstance::new( - azalea_world::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 world - Some(player_entity), - ); - local_player.world = instance; - - let player_bundle = PlayerBundle { - entity: EntityBundle::new( - game_profile.uuid, - Vec3::default(), - azalea_registry::EntityKind::Player, - new_instance_name, - ), - metadata: PlayerMetadataBundle::default(), - }; - // insert our components into the ecs :) - commands.entity(player_entity).insert(( - MinecraftEntityId(p.player_id), - LocalGameMode { - current: p.game_type, - previous: p.previous_game_type.into(), - }, - // this gets overwritten later by the SetHealth packet - received_registries, - player_bundle, - )); - - // add our own player to our index - entity_id_index.insert(MinecraftEntityId(p.player_id), player_entity); - } - - // brand - let mut brand_data = Vec::new(); - // they don't have to know :) - "vanilla".write_into(&mut brand_data).unwrap(); - local_player.write_packet( - ServerboundCustomPayloadPacket { - identifier: ResourceLocation::new("brand"), - data: brand_data.into(), - } - .get(), - ); - - // send the client information that we have set - log::debug!( - "Sending client information because login: {:?}", - client_information - ); - local_player.write_packet(client_information.clone().get()); - - system_state.apply(ecs); - } - ClientboundGamePacket::SetChunkCacheRadius(p) => { - debug!("Got set chunk cache radius packet {:?}", p); - } - 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::SetCarriedItem(p) => { - debug!("Got set carried 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, - }); - // bye - return; - } - ClientboundGamePacket::UpdateRecipes(_p) => { - debug!("Got update recipes packet"); - } - ClientboundGamePacket::EntityEvent(_p) => { - // debug!("Got entity event packet {:?}", p); - } - ClientboundGamePacket::Recipe(_p) => { - debug!("Got recipe packet"); - } - ClientboundGamePacket::PlayerPosition(p) => { - // TODO: reply with teleport confirm - debug!("Got player position packet {:?}", p); - - #[allow(clippy::type_complexity)] - let mut system_state: SystemState< - Query<( - &mut LocalPlayer, - &mut Physics, - &mut LookDirection, - &mut Position, - &mut LastSentPosition, - )>, - > = SystemState::new(ecs); - let mut query = system_state.get_mut(ecs); - let Ok(( - local_player, - mut physics, - mut direction, - mut position, - mut last_sent_position, - )) = query.get_mut(player_entity) - else { - continue; - }; - - let delta_movement = physics.delta; - - let is_x_relative = p.relative_arguments.x; - let is_y_relative = p.relative_arguments.y; - let is_z_relative = p.relative_arguments.z; - - let (delta_x, new_pos_x) = if is_x_relative { - last_sent_position.x += p.x; - (delta_movement.x, position.x + p.x) - } else { - last_sent_position.x = p.x; - (0.0, p.x) - }; - let (delta_y, new_pos_y) = if is_y_relative { - last_sent_position.y += p.y; - (delta_movement.y, position.y + p.y) - } else { - last_sent_position.y = p.y; - (0.0, p.y) - }; - let (delta_z, new_pos_z) = if is_z_relative { - last_sent_position.z += p.z; - (delta_movement.z, position.z + p.z) - } else { - last_sent_position.z = p.z; - (0.0, p.z) - }; - - let mut y_rot = p.y_rot; - let mut x_rot = p.x_rot; - if p.relative_arguments.x_rot { - x_rot += direction.x_rot; - } - if p.relative_arguments.y_rot { - y_rot += direction.y_rot; - } - - physics.delta = Vec3 { - x: delta_x, - y: delta_y, - z: delta_z, - }; - // we call a function instead of setting the fields ourself since the - // function makes sure the rotations stay in their - // ranges - (direction.y_rot, direction.x_rot) = (y_rot, x_rot); - // TODO: minecraft sets "xo", "yo", and "zo" here but idk what that means - // so investigate that ig - let new_pos = Vec3 { - x: new_pos_x, - y: new_pos_y, - z: new_pos_z, - }; - - if new_pos != **position { - **position = new_pos; - } - - local_player.write_packet(ServerboundAcceptTeleportationPacket { id: p.id }.get()); - local_player.write_packet( - ServerboundMovePlayerPosRotPacket { - x: new_pos.x, - y: new_pos.y, - z: new_pos.z, - y_rot, - x_rot, - // this is always false - on_ground: false, - } - .get(), - ); - } - 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 = updated_info.display_name.clone(); - } - update_player_events.send(UpdatePlayerEvent { - entity: player_entity, - info: info.clone(), - }); - } else { - warn!( - "Ignoring PlayerInfoUpdate for unknown player {}", - updated_info.profile.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 local_player = query.get_mut(player_entity).unwrap(); - let mut partial_world = local_player.partial_instance.write(); - - partial_world.chunks.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 pos = ChunkPos::new(p.x, p.z); - - 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(); - - // OPTIMIZATION: if we already know about the chunk from the - // shared world (and not ourselves), then we don't need to - // parse it again. This is only used when we have a shared - // world, since we check that the chunk isn't currently owned - // by this client. - let shared_chunk = local_player.world.read().chunks.get(&pos); - let this_client_has_chunk = local_player - .partial_instance - .read() - .chunks - .limited_get(&pos) - .is_some(); - - let mut world = local_player.world.write(); - let mut partial_world = local_player.partial_instance.write(); - - if !this_client_has_chunk { - if let Some(shared_chunk) = shared_chunk { - trace!( - "Skipping parsing chunk {:?} because we already know about it", - pos - ); - partial_world.chunks.set_with_shared_reference( - &pos, - Some(shared_chunk.clone()), - &mut world.chunks, - ); - continue; - } - } - - let heightmaps = p - .chunk_data - .heightmaps - .as_compound() - .and_then(|c| c.get("")) - .and_then(|c| c.as_compound()); - // necessary to make the unwrap_or work - let empty_nbt_compound = NbtCompound::default(); - let heightmaps = heightmaps.unwrap_or(&empty_nbt_compound); - - if let Err(e) = partial_world.chunks.replace_with_packet_data( - &pos, - &mut Cursor::new(&p.chunk_data.data), - heightmaps, - &mut world.chunks, - ) { - error!("Couldn't set chunk data: {}", e); - } - } - ClientboundGamePacket::AddEntity(p) => { - debug!("Got add entity packet {p:?}"); - - #[allow(clippy::type_complexity)] - let mut system_state: SystemState<( - Commands, - Query<(&mut EntityIdIndex, Option<&InstanceName>)>, - Res, - ResMut, - )> = SystemState::new(ecs); - let (mut commands, mut query, instance_container, mut entity_uuid_index) = - system_state.get_mut(ecs); - let (mut entity_id_index, instance_name) = query.get_mut(player_entity).unwrap(); - - if let Some(instance_name) = instance_name { - let bundle = p.as_entity_bundle((**instance_name).clone()); - let mut spawned = commands.spawn(( - MinecraftEntityId(p.id), - LoadedBy(HashSet::from([player_entity])), - bundle, - )); - entity_id_index.insert(MinecraftEntityId(p.id), spawned.id()); - - { - // add it to the indexes immediately so if there's a packet that references - // it immediately after it still works - let instance = instance_container.get(instance_name).unwrap(); - instance - .write() - .entity_by_id - .insert(MinecraftEntityId(p.id), spawned.id()); - entity_uuid_index.insert(p.uuid, spawned.id()); - } - - // the bundle doesn't include the default entity metadata so we add that - // separately - p.apply_metadata(&mut spawned); - } else { - warn!("got add player packet but we haven't gotten a login packet yet"); - } - - 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, &LocalPlayer)>, - Query<&EntityKind>, - )> = SystemState::new(ecs); - let (mut commands, mut query, entity_kind_query) = system_state.get_mut(ecs); - let (entity_id_index, local_player) = query.get_mut(player_entity).unwrap(); - - let entity = entity_id_index.get(&MinecraftEntityId(p.id)); - - let Some(entity) = entity else { - warn!("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(); - - // we use RelativeEntityUpdate because it makes sure changes aren't made - // multiple times - commands.entity(entity).add(RelativeEntityUpdate { - partial_world: local_player.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_comands = commands.entity(entity_id); - if let Err(e) = apply_metadata( - &mut entity_comands, - *entity_kind, - (*p.packed_items).clone(), - ) { - warn!("{e}"); - } - }); - }), - }); - - system_state.apply(ecs); - } - ClientboundGamePacket::UpdateAttributes(_p) => { - // debug!("Got update attributes packet {:?}", p); - } - ClientboundGamePacket::SetEntityMotion(_p) => { - // debug!("Got entity velocity packet {:?}", p); - } - ClientboundGamePacket::SetEntityLink(p) => { - debug!("Got set entity link packet {p:?}"); - } - ClientboundGamePacket::AddPlayer(p) => { - debug!("Got add player packet {p:?}"); - - #[allow(clippy::type_complexity)] - let mut system_state: SystemState<( - Commands, - Query<(&mut EntityIdIndex, &TabList, Option<&InstanceName>)>, - )> = SystemState::new(ecs); - let (mut commands, mut query) = system_state.get_mut(ecs); - let (mut entity_id_index, tab_list, world_name) = - query.get_mut(player_entity).unwrap(); - - if let Some(InstanceName(world_name)) = world_name { - let bundle = p.as_player_bundle(world_name.clone()); - let mut spawned = commands.spawn(( - MinecraftEntityId(p.id), - LoadedBy(HashSet::from([player_entity])), - bundle, - )); - entity_id_index.insert(MinecraftEntityId(p.id), spawned.id()); - - if let Some(player_info) = tab_list.get(&p.uuid) { - spawned.insert(GameProfileComponent(player_info.profile.clone())); - } - } else { - warn!("got add player packet but we haven't gotten a login packet yet"); - } - - system_state.apply(ecs); - } - 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, &LocalPlayer)>, - )> = SystemState::new(ecs); - let (mut commands, mut query) = system_state.get_mut(ecs); - let (entity_id_index, local_player) = query.get_mut(player_entity).unwrap(); - - let entity = entity_id_index.get(&MinecraftEntityId(p.id)); - - if let Some(entity) = entity { - let new_pos = p.position; - commands.entity(entity).add(RelativeEntityUpdate { - partial_world: local_player.partial_instance.clone(), - update: Box::new(move |entity| { - let mut position = entity.get_mut::().unwrap(); - if new_pos != **position { - **position = new_pos; - } - }), - }); - } else { - warn!("Got teleport entity packet for unknown entity id {}", p.id); - } - - 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, &LocalPlayer)>, - )> = SystemState::new(ecs); - let (mut commands, mut query) = system_state.get_mut(ecs); - let (entity_id_index, local_player) = query.get_mut(player_entity).unwrap(); - - let entity = entity_id_index.get(&MinecraftEntityId(p.entity_id)); - - if let Some(entity) = entity { - let delta = p.delta.clone(); - commands.entity(entity).add(RelativeEntityUpdate { - partial_world: local_player.partial_instance.clone(), - update: Box::new(move |entity_mut| { - let mut position = entity_mut.get_mut::().unwrap(); - let new_pos = position.with_delta(&delta); - if new_pos != **position { - **position = new_pos; - } - }), - }); - } else { - warn!( - "Got move entity pos packet for unknown entity id {}", - p.entity_id - ); - } - - system_state.apply(ecs); - } - ClientboundGamePacket::MoveEntityPosRot(p) => { - let mut system_state: SystemState<( - Commands, - Query<(&EntityIdIndex, &LocalPlayer)>, - )> = SystemState::new(ecs); - let (mut commands, mut query) = system_state.get_mut(ecs); - let (entity_id_index, local_player) = query.get_mut(player_entity).unwrap(); - - let entity = entity_id_index.get(&MinecraftEntityId(p.entity_id)); - - if let Some(entity) = entity { - let delta = p.delta.clone(); - commands.entity(entity).add(RelativeEntityUpdate { - partial_world: local_player.partial_instance.clone(), - update: Box::new(move |entity_mut| { - let mut position = entity_mut.get_mut::().unwrap(); - let new_pos = position.with_delta(&delta); - if new_pos != **position { - **position = new_pos; - } - }), - }); - } else { - warn!( - "Got move entity pos rot packet for unknown entity id {}", - p.entity_id - ); - } - - system_state.apply(ecs); - } - - ClientboundGamePacket::MoveEntityRot(_p) => { - // debug!("Got move entity rot packet {:?}", p); - } - ClientboundGamePacket::KeepAlive(p) => { - debug!("Got keep alive packet {p:?} for {player_entity:?}"); - - let mut system_state: SystemState<( - Query<&mut LocalPlayer>, - EventWriter, - )> = SystemState::new(ecs); - let (mut query, mut keepalive_events) = system_state.get_mut(ecs); - - keepalive_events.send(KeepAliveEvent { - entity: player_entity, - id: p.id, - }); - - let local_player = query.get_mut(player_entity).unwrap(); - local_player.write_packet(ServerboundKeepAlivePacket { id: p.id }.get()); - debug!("Sent keep alive packet {p:?} for {player_entity:?}"); - } - 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(&MinecraftEntityId(id)) else { - warn!("There is no entity with id {id:?}"); - 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; - }; - 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::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.world.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.world.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::clientbound_game_event_packet::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 InventoryComponent>, - 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 as u8, - }); - } - } - 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 as i8) - && (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(_) => {} - ClientboundGamePacket::ForgetLevelChunk(_) => {} - 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, - }) - } - ClientboundGamePacket::OpenSignEditor(_) => {} - ClientboundGamePacket::Ping(p) => { - trace!("Got ping 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(); - local_player.write_packet(ServerboundPongPacket { id: p.id }.get()); - } - 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::ResourcePack(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, - url: p.url, - hash: p.hash, - required: p.required, - prompt: p.prompt, - }); - - system_state.apply(ecs); - } - ClientboundGamePacket::Respawn(p) => { - debug!("Got respawn packet {:?}", p); - - #[allow(clippy::type_complexity)] - let mut system_state: SystemState<( - Commands, - Query<( - &mut LocalPlayer, - &GameProfileComponent, - &ClientInformation, - &ReceivedRegistries, - )>, - EventWriter, - ResMut, - )> = SystemState::new(ecs); - let (mut commands, mut query, mut instance_loaded_events, mut instance_container) = - system_state.get_mut(ecs); - let (mut local_player, game_profile, client_information, received_registries) = - query.get_mut(player_entity).unwrap(); - - { - let dimension = &received_registries - .dimension_type - .value - .iter() - .find(|t| t.name == p.dimension_type) - .unwrap_or_else(|| { - panic!("No dimension_type with name {}", p.dimension_type) - }) - .element; - - let new_instance_name = p.dimension.clone(); - - // add this world to the instance_container (or don't if it's already - // there) - let instance = instance_container.insert( - new_instance_name.clone(), - dimension.height, - dimension.min_y, - ); - instance_loaded_events.send(InstanceLoadedEvent { - entity: player_entity, - name: new_instance_name.clone(), - instance: Arc::downgrade(&instance), - }); - - // set the partial_world to an empty world - // (when we add chunks or entities those will be in the - // instance_container) - *local_player.partial_instance.write() = PartialInstance::new( - azalea_world::calculate_chunk_storage_range( - client_information.view_distance.into(), - ), - Some(player_entity), - ); - local_player.world = instance; - - // this resets a bunch of our components like physics and stuff - let player_bundle = PlayerBundle { - entity: EntityBundle::new( - game_profile.uuid, - Vec3::default(), - azalea_registry::EntityKind::Player, - new_instance_name, - ), - metadata: PlayerMetadataBundle::default(), - }; - // update the local gamemode and metadata things - commands.entity(player_entity).insert(( - LocalGameMode { - current: p.game_type, - previous: p.previous_game_type.into(), - }, - player_bundle, - )); - } - - // Remove the Dead marker component from the player. - commands.entity(player_entity).remove::(); - - 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::DisguisedChat(_) => {} - ClientboundGamePacket::UpdateEnabledFeatures(_) => {} - ClientboundGamePacket::Bundle(_) => {} - ClientboundGamePacket::DamageEvent(_) => {} - ClientboundGamePacket::HurtAnimation(_) => {} - } - } -} - -impl PacketReceiver { - /// Loop that reads from the connection and adds the packets to the queue + - /// runs the schedule. - pub async fn read_task(self, mut read_conn: ReadConnection) { - loop { - match read_conn.read().await { - Ok(packet) => { - self.packets.lock().push(packet); - // tell the client to run all the systems - self.run_schedule_sender.send(()).unwrap(); - } - Err(error) => { - if !matches!(*error, ReadPacketError::ConnectionClosed) { - error!("Error reading packet from Client: {error:?}"); - } - break; - } - } - } - } - - /// Consume the [`ServerboundGamePacket`] queue and actually write the - /// packets to the server. It's like this so writing packets doesn't need to - /// be awaited. - pub async fn write_task( - self, - mut write_conn: WriteConnection, - mut write_receiver: mpsc::UnboundedReceiver, - ) { - while let Some(packet) = write_receiver.recv().await { - if let Err(err) = write_conn.write(packet).await { - error!("Disconnecting because we couldn't write a packet: {err}."); - break; - }; - } - // receiver is automatically closed when it's dropped - } -} diff --git a/azalea-client/src/packet_handling/configuration.rs b/azalea-client/src/packet_handling/configuration.rs new file mode 100644 index 00000000..6930e739 --- /dev/null +++ b/azalea-client/src/packet_handling/configuration.rs @@ -0,0 +1,204 @@ +use std::io::Cursor; +use std::sync::Arc; + +use azalea_entity::indexing::{EntityIdIndex, Loaded}; +use azalea_protocol::packets::configuration::serverbound_finish_configuration_packet::ServerboundFinishConfigurationPacket; +use azalea_protocol::packets::configuration::serverbound_keep_alive_packet::ServerboundKeepAlivePacket; +use azalea_protocol::packets::configuration::serverbound_pong_packet::ServerboundPongPacket; +use azalea_protocol::packets::configuration::serverbound_resource_pack_packet::ServerboundResourcePackPacket; +use azalea_protocol::packets::configuration::ClientboundConfigurationPacket; +use azalea_protocol::packets::ConnectionProtocol; +use azalea_protocol::read::deserialize_packet; +use azalea_world::Instance; +use bevy_ecs::prelude::*; +use bevy_ecs::system::SystemState; +use log::{debug, error, warn}; +use parking_lot::RwLock; + +use crate::client::InConfigurationState; +use crate::disconnect::DisconnectEvent; +use crate::local_player::Hunger; +use crate::packet_handling::game::KeepAliveEvent; +use crate::raw_connection::RawConnection; +use crate::ReceivedRegistries; + +#[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: ClientboundConfigurationPacket, +} + +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); + continue; + } + }; + packet_events.send(PacketEvent { + entity: player_entity, + packet: packet.clone(), + }); + } + // 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 PacketEvent { + entity: player_entity, + packet, + } in events.iter() + { + // 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 { + ClientboundConfigurationPacket::RegistryData(p) => { + let mut system_state: SystemState> = + SystemState::new(ecs); + let mut query = system_state.get_mut(ecs); + let mut received_registries = query.get_mut(player_entity).unwrap(); + + let new_received_registries = p.registry_holder.registries; + // override the old registries with the new ones + // but if a registry wasn't sent, keep the old one + for (registry_name, registry) in new_received_registries { + received_registries + .registries + .insert(registry_name, registry); + } + } + + ClientboundConfigurationPacket::CustomPayload(p) => { + debug!("Got custom payload packet {p:?}"); + } + ClientboundConfigurationPacket::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, + }); + } + ClientboundConfigurationPacket::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_connection = query.get_mut(player_entity).unwrap(); + + let instance_holder = crate::local_player::InstanceHolder::new( + player_entity, + // default to an empty world, it'll be set correctly later when we + // get the login packet + Arc::new(RwLock::new(Instance::default())), + ); + + raw_connection + .write_packet(ServerboundFinishConfigurationPacket {}.get()) + .expect( + "we should be in the right state and encoding this packet shouldn't fail", + ); + raw_connection.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 { + instance_holder, + physics_state: crate::PhysicsState::default(), + inventory: crate::inventory::InventoryComponent::default(), + client_information: crate::ClientInformation::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::chunk_batching::ChunkBatchInfo::default(), + + entity_id_index: EntityIdIndex::default(), + + mining: crate::mining::MineBundle::default(), + attack: crate::attack::AttackBundle::default(), + + _local_entity: azalea_entity::LocalEntity, + _loaded: Loaded, + }); + } + ClientboundConfigurationPacket::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_connection = query.get(player_entity).unwrap(); + + keepalive_events.send(KeepAliveEvent { + entity: player_entity, + id: p.id, + }); + raw_connection + .write_packet(ServerboundKeepAlivePacket { id: p.id }.get()) + .unwrap(); + } + ClientboundConfigurationPacket::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_connection = query.get_mut(player_entity).unwrap(); + + raw_connection + .write_packet(ServerboundPongPacket { id: p.id }.get()) + .unwrap(); + } + ClientboundConfigurationPacket::ResourcePack(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_connection = query.get_mut(player_entity).unwrap(); + + // always accept resource pack + raw_connection.write_packet( + ServerboundResourcePackPacket { action: azalea_protocol::packets::configuration::serverbound_resource_pack_packet::Action::Accepted }.get() + ).unwrap(); + } + ClientboundConfigurationPacket::UpdateEnabledFeatures(p) => { + debug!("Got update enabled features packet {p:?}"); + } + ClientboundConfigurationPacket::UpdateTags(_p) => { + debug!("Got update tags packet"); + } + } + } +} diff --git a/azalea-client/src/packet_handling/game.rs b/azalea-client/src/packet_handling/game.rs new file mode 100644 index 00000000..e0a8b017 --- /dev/null +++ b/azalea-client/src/packet_handling/game.rs @@ -0,0 +1,1295 @@ +use std::{ + collections::HashSet, + io::Cursor, + sync::{Arc, Weak}, +}; + +use azalea_chat::FormattedText; +use azalea_core::{ChunkPos, GameMode, ResourceLocation, Vec3}; +use azalea_entity::{ + indexing::{EntityIdIndex, EntityUuidIndex}, + metadata::{apply_metadata, Health, PlayerMetadataBundle}, + Dead, EntityBundle, EntityKind, LastSentPosition, LoadedBy, LocalEntity, LookDirection, + Physics, PlayerBundle, Position, RelativeEntityUpdate, +}; +use azalea_nbt::NbtCompound; +use azalea_protocol::{ + packets::game::{ + clientbound_player_combat_kill_packet::ClientboundPlayerCombatKillPacket, + serverbound_accept_teleportation_packet::ServerboundAcceptTeleportationPacket, + serverbound_keep_alive_packet::ServerboundKeepAlivePacket, + serverbound_move_player_pos_rot_packet::ServerboundMovePlayerPosRotPacket, + serverbound_pong_packet::ServerboundPongPacket, ClientboundGamePacket, + }, + read::deserialize_packet, +}; +use azalea_world::{Instance, InstanceContainer, InstanceName, MinecraftEntityId, PartialInstance}; +use bevy_ecs::{prelude::*, system::SystemState}; +use log::{debug, error, trace, warn}; +use parking_lot::RwLock; + +use crate::{ + chat::{ChatPacket, ChatReceivedEvent}, + chunk_batching, + disconnect::DisconnectEvent, + inventory::{ + ClientSideCloseContainerEvent, InventoryComponent, MenuOpenedEvent, + SetContainerContentEvent, + }, + local_player::{ + GameProfileComponent, Hunger, InstanceHolder, LocalGameMode, PlayerAbilities, + SendPacketEvent, TabList, + }, + raw_connection::RawConnection, + ClientInformation, PlayerInfo, ReceivedRegistries, +}; + +/// 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.iter() { +/// match packet { +/// 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: ClientboundGamePacket, +} + +/// 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 [`ClientboundPlayerCombatKillPacket`] 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, + 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); + continue; + } + }; + packet_events.send(PacketEvent { + entity: player_entity, + packet: packet.clone(), + }); + } + // 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 PacketEvent { + entity: player_entity, + packet, + } in events.iter() + { + // 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 { + ClientboundGamePacket::Login(p) => { + debug!("Got login packet"); + + #[allow(clippy::type_complexity)] + let mut system_state: SystemState<( + Commands, + Query<( + &GameProfileComponent, + &ClientInformation, + &ReceivedRegistries, + Option<&mut InstanceName>, + &mut EntityIdIndex, + &mut InstanceHolder, + )>, + EventWriter, + ResMut, + EventWriter, + )> = SystemState::new(ecs); + let ( + mut commands, + mut query, + mut instance_loaded_events, + mut instance_container, + mut send_packet_events, + ) = system_state.get_mut(ecs); + let ( + game_profile, + client_information, + received_registries, + instance_name, + 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) = received_registries.dimension_type() else { + error!("Server didn't send dimension type registry, can't log in"); + continue; + }; + let dimension = &dimension_type + .value + .iter() + .find(|t| t.name == p.common.dimension_type) + .unwrap_or_else(|| { + panic!("No dimension_type with name {}", p.common.dimension_type) + }) + .element; + + // 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.height, + dimension.min_y, + ); + 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::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), + ); + instance_holder.instance = weak_instance; + + let player_bundle = PlayerBundle { + entity: EntityBundle::new( + game_profile.uuid, + Vec3::default(), + azalea_registry::EntityKind::Player, + new_instance_name, + ), + metadata: PlayerMetadataBundle::default(), + }; + // insert our components into the ecs :) + commands.entity(player_entity).insert(( + MinecraftEntityId(p.player_id), + LocalGameMode { + current: p.common.game_type, + previous: p.common.previous_game_type.into(), + }, + // this gets overwritten later by the SetHealth packet + received_registries.clone(), + player_bundle, + )); + + // add our own player to our index + entity_id_index.insert(MinecraftEntityId(p.player_id), player_entity); + } + + // send the client information that we have set + debug!( + "Sending client information because login: {:?}", + client_information + ); + send_packet_events.send(SendPacketEvent { + entity: player_entity, + packet: azalea_protocol::packets::game::serverbound_client_information_packet::ServerboundClientInformationPacket { information: client_information.clone() }.get(), + }); + + 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< + EventWriter, + > = SystemState::new(ecs); + let mut chunk_batch_start_events = system_state.get_mut(ecs); + + chunk_batch_start_events.send(chunk_batching::ChunkBatchStartEvent { + entity: player_entity, + }); + } + ClientboundGamePacket::ChunkBatchFinished(p) => { + debug!("Got chunk batch finished {p:?}"); + + let mut system_state: SystemState< + EventWriter, + > = SystemState::new(ecs); + let mut chunk_batch_start_events = system_state.get_mut(ecs); + + chunk_batch_start_events.send(chunk_batching::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::SetCarriedItem(p) => { + debug!("Got set carried 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, + }); + } + ClientboundGamePacket::UpdateRecipes(_p) => { + debug!("Got update recipes packet"); + } + ClientboundGamePacket::EntityEvent(_p) => { + // debug!("Got entity event packet {p:?}"); + } + ClientboundGamePacket::Recipe(_p) => { + debug!("Got recipe packet"); + } + ClientboundGamePacket::PlayerPosition(p) => { + // TODO: reply with teleport confirm + 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; + }; + + let delta_movement = physics.delta; + + let is_x_relative = p.relative_arguments.x; + let is_y_relative = p.relative_arguments.y; + let is_z_relative = p.relative_arguments.z; + + let (delta_x, new_pos_x) = if is_x_relative { + last_sent_position.x += p.x; + (delta_movement.x, position.x + p.x) + } else { + last_sent_position.x = p.x; + (0.0, p.x) + }; + let (delta_y, new_pos_y) = if is_y_relative { + last_sent_position.y += p.y; + (delta_movement.y, position.y + p.y) + } else { + last_sent_position.y = p.y; + (0.0, p.y) + }; + let (delta_z, new_pos_z) = if is_z_relative { + last_sent_position.z += p.z; + (delta_movement.z, position.z + p.z) + } else { + last_sent_position.z = p.z; + (0.0, p.z) + }; + + let mut y_rot = p.y_rot; + let mut x_rot = p.x_rot; + if p.relative_arguments.x_rot { + x_rot += direction.x_rot; + } + if p.relative_arguments.y_rot { + y_rot += direction.y_rot; + } + + physics.delta = Vec3 { + x: delta_x, + y: delta_y, + z: delta_z, + }; + // we call a function instead of setting the fields ourself since the + // function makes sure the rotations stay in their + // ranges + (direction.y_rot, direction.x_rot) = (y_rot, x_rot); + // TODO: minecraft sets "xo", "yo", and "zo" here but idk what that means + // so investigate that ig + let new_pos = Vec3 { + x: new_pos_x, + y: new_pos_y, + z: new_pos_z, + }; + + if new_pos != **position { + **position = new_pos; + } + + send_packet_events.send(SendPacketEvent { + entity: player_entity, + packet: ServerboundAcceptTeleportationPacket { id: p.id }.get(), + }); + send_packet_events.send(SendPacketEvent { + entity: player_entity, + packet: ServerboundMovePlayerPosRotPacket { + x: new_pos.x, + y: new_pos.y, + z: new_pos.z, + y_rot, + x_rot, + // this is always false + on_ground: false, + } + .get(), + }); + } + 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 = updated_info.display_name.clone(); + } + update_player_events.send(UpdatePlayerEvent { + entity: player_entity, + info: info.clone(), + }); + } else { + warn!( + "Ignoring PlayerInfoUpdate for unknown player {}", + updated_info.profile.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 local_player = query.get_mut(player_entity).unwrap(); + let mut partial_world = local_player.partial_instance.write(); + + partial_world.chunks.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 pos = ChunkPos::new(p.x, p.z); + + 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(); + + // OPTIMIZATION: if we already know about the chunk from the + // shared world (and not ourselves), then we don't need to + // parse it again. This is only used when we have a shared + // world, since we check that the chunk isn't currently owned + // by this client. + let shared_chunk = local_player.instance.read().chunks.get(&pos); + let this_client_has_chunk = local_player + .partial_instance + .read() + .chunks + .limited_get(&pos) + .is_some(); + + let mut world = local_player.instance.write(); + let mut partial_world = local_player.partial_instance.write(); + + if !this_client_has_chunk { + if let Some(shared_chunk) = shared_chunk { + trace!( + "Skipping parsing chunk {:?} because we already know about it", + pos + ); + partial_world.chunks.set_with_shared_reference( + &pos, + Some(shared_chunk.clone()), + &mut world.chunks, + ); + continue; + } + } + + let heightmaps = p + .chunk_data + .heightmaps + .as_compound() + .and_then(|c| c.get("")) + .and_then(|c| c.as_compound()); + // necessary to make the unwrap_or work + let empty_nbt_compound = NbtCompound::default(); + let heightmaps = heightmaps.unwrap_or(&empty_nbt_compound); + + if let Err(e) = partial_world.chunks.replace_with_packet_data( + &pos, + &mut Cursor::new(&p.chunk_data.data), + heightmaps, + &mut world.chunks, + ) { + error!("Couldn't set chunk data: {}", e); + } + } + 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>)>, + Res, + ResMut, + )> = SystemState::new(ecs); + let (mut commands, mut 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(); + + if let Some(instance_name) = instance_name { + let bundle = p.as_entity_bundle((**instance_name).clone()); + let mut spawned = commands.spawn(( + MinecraftEntityId(p.id), + LoadedBy(HashSet::from([player_entity])), + bundle, + )); + entity_id_index.insert(MinecraftEntityId(p.id), spawned.id()); + + { + // add it to the indexes immediately so if there's a packet that references + // it immediately after it still works + let instance = instance_container.get(instance_name).unwrap(); + instance + .write() + .entity_by_id + .insert(MinecraftEntityId(p.id), spawned.id()); + entity_uuid_index.insert(p.uuid, spawned.id()); + } + + 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); + } else { + warn!("got add player packet but we haven't gotten a login packet yet"); + } + + 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(&MinecraftEntityId(p.id)); + + let Some(entity) = entity else { + warn!("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(); + + // we use RelativeEntityUpdate because it makes sure changes aren't made + // multiple times + commands.entity(entity).add(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_comands = commands.entity(entity_id); + if let Err(e) = apply_metadata( + &mut entity_comands, + *entity_kind, + (*p.packed_items).clone(), + ) { + warn!("{e}"); + } + }); + }), + }); + + system_state.apply(ecs); + } + ClientboundGamePacket::UpdateAttributes(_p) => { + // debug!("Got update attributes packet {p:?}"); + } + ClientboundGamePacket::SetEntityMotion(_p) => { + // debug!("Got entity velocity packet {p:?}"); + } + 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 entity = entity_id_index.get(&MinecraftEntityId(p.id)); + + if let Some(entity) = entity { + let new_pos = p.position; + commands.entity(entity).add(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; + } + }), + }); + } else { + warn!("Got teleport entity packet for unknown entity id {}", p.id); + } + + 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(); + + let entity = entity_id_index.get(&MinecraftEntityId(p.entity_id)); + + if let Some(entity) = entity { + let delta = p.delta.clone(); + commands.entity(entity).add(RelativeEntityUpdate { + partial_world: instance_holder.partial_instance.clone(), + update: Box::new(move |entity_mut| { + let mut position = entity_mut.get_mut::().unwrap(); + let new_pos = position.with_delta(&delta); + if new_pos != **position { + **position = new_pos; + } + }), + }); + } else { + warn!( + "Got move entity pos packet for unknown entity id {}", + p.entity_id + ); + } + + 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(); + + let entity = entity_id_index.get(&MinecraftEntityId(p.entity_id)); + + if let Some(entity) = entity { + let delta = p.delta.clone(); + commands.entity(entity).add(RelativeEntityUpdate { + partial_world: instance_holder.partial_instance.clone(), + update: Box::new(move |entity_mut| { + let mut position = entity_mut.get_mut::().unwrap(); + let new_pos = position.with_delta(&delta); + if new_pos != **position { + **position = new_pos; + } + }), + }); + } else { + warn!( + "Got move entity pos rot packet for unknown entity id {}", + p.entity_id + ); + } + + system_state.apply(ecs); + } + + ClientboundGamePacket::MoveEntityRot(_p) => { + // debug!("Got move entity rot packet {p:?}"); + } + 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 { + entity: player_entity, + packet: ServerboundKeepAlivePacket { id: p.id }.get(), + }); + } + 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(&MinecraftEntityId(id)) else { + warn!("There is no entity with id {id:?}"); + 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; + }; + 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::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::clientbound_game_event_packet::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 InventoryComponent>, + 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 as u8, + }); + } + } + 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 as i8) + && (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(_) => {} + ClientboundGamePacket::ForgetLevelChunk(_) => {} + 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, + }) + } + 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 { + entity: player_entity, + packet: ServerboundPongPacket { id: p.id }.get(), + }); + } + ClientboundGamePacket::PongResponse(p) => { + debug!("Got pong response packet {p:?}"); + } + 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::ResourcePack(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, + url: p.url, + hash: p.hash, + required: p.required, + prompt: p.prompt, + }); + + system_state.apply(ecs); + } + ClientboundGamePacket::Respawn(p) => { + debug!("Got respawn packet {p:?}"); + + #[allow(clippy::type_complexity)] + let mut system_state: SystemState<( + Commands, + Query<( + &mut InstanceHolder, + &GameProfileComponent, + &ClientInformation, + &ReceivedRegistries, + )>, + 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, received_registries) = + query.get_mut(player_entity).unwrap(); + + { + let new_instance_name = p.common.dimension.clone(); + + let Some(dimension_type) = received_registries.dimension_type() else { + error!("Server didn't send dimension type registry, can't log in"); + continue; + }; + + let dimension = &dimension_type + .value + .iter() + .find(|t| t.name == p.common.dimension_type) + .unwrap_or_else(|| { + panic!("No dimension_type with name {}", p.common.dimension_type) + }) + .element; + + // 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.height, + dimension.min_y, + ); + 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::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 player_bundle = PlayerBundle { + entity: EntityBundle::new( + game_profile.uuid, + Vec3::default(), + azalea_registry::EntityKind::Player, + new_instance_name, + ), + metadata: PlayerMetadataBundle::default(), + }; + // 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(), + }, + player_bundle, + )); + } + + // Remove the Dead marker component from the player. + commands.entity(player_entity).remove::(); + + 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::DisguisedChat(_) => {} + ClientboundGamePacket::Bundle(_) => {} + ClientboundGamePacket::DamageEvent(_) => {} + ClientboundGamePacket::HurtAnimation(_) => {} + + ClientboundGamePacket::StartConfiguration(_) => todo!(), + } + } +} diff --git a/azalea-client/src/packet_handling/mod.rs b/azalea-client/src/packet_handling/mod.rs new file mode 100644 index 00000000..35bdfc04 --- /dev/null +++ b/azalea-client/src/packet_handling/mod.rs @@ -0,0 +1,59 @@ +use azalea_entity::{metadata::Health, EntityUpdateSet}; +use bevy_app::{App, First, Plugin, PreUpdate, Update}; +use bevy_ecs::prelude::*; + +use crate::{chat::ChatReceivedEvent, events::death_listener}; + +use self::game::{ + AddPlayerEvent, DeathEvent, InstanceLoadedEvent, KeepAliveEvent, RemovePlayerEvent, + ResourcePackEvent, UpdatePlayerEvent, +}; + +pub mod configuration; +pub mod game; + +pub struct PacketHandlerPlugin; + +pub fn death_event_on_0_health( + query: Query<(Entity, &Health), Changed>, + mut death_events: EventWriter, +) { + for (entity, health) in query.iter() { + if **health == 0. { + death_events.send(DeathEvent { + entity, + packet: None, + }); + } + } +} + +impl Plugin for PacketHandlerPlugin { + fn build(&self, app: &mut App) { + app.add_systems( + First, + (game::send_packet_events, configuration::send_packet_events), + ) + .add_systems( + PreUpdate, + ( + game::process_packet_events, + configuration::process_packet_events, + ) + // we want to index and deindex right after + .before(EntityUpdateSet::Deindex), + ) + .add_systems(Update, death_event_on_0_health.before(death_listener)) + // we do this instead of add_event so we can handle the events ourselves + .init_resource::>() + .init_resource::>() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::(); + } +} diff --git a/azalea-client/src/ping.rs b/azalea-client/src/ping.rs index 8acde7a5..9064065c 100755 --- a/azalea-client/src/ping.rs +++ b/azalea-client/src/ping.rs @@ -3,7 +3,7 @@ use azalea_protocol::{ connect::{Connection, ConnectionError}, packets::{ - handshake::client_intention_packet::ClientIntentionPacket, + handshaking::client_intention_packet::ClientIntentionPacket, status::{ clientbound_status_response_packet::ClientboundStatusResponsePacket, serverbound_status_request_packet::ServerboundStatusRequestPacket, diff --git a/azalea-client/src/player.rs b/azalea-client/src/player.rs index a94340ab..1aba172a 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::{packet_handling::AddPlayerEvent, GameProfileComponent}; +use crate::{packet_handling::game::AddPlayerEvent, GameProfileComponent}; /// A player in the tab list. #[derive(Debug, Clone)] diff --git a/azalea-client/src/raw_connection.rs b/azalea-client/src/raw_connection.rs new file mode 100644 index 00000000..0df13a60 --- /dev/null +++ b/azalea-client/src/raw_connection.rs @@ -0,0 +1,174 @@ +use std::fmt::Debug; +use std::sync::Arc; + +use azalea_protocol::{ + connect::{RawReadConnection, RawWriteConnection}, + packets::{ConnectionProtocol, ProtocolPacket}, + read::ReadPacketError, + write::serialize_packet, +}; +use bevy_ecs::prelude::*; +use log::error; +use parking_lot::Mutex; +use thiserror::Error; +use tokio::sync::mpsc; + +/// A component for clients that can read and write packets to the server. This +/// works with raw bytes, so you'll have to serialize/deserialize packets +/// yourself. It will do the compression and encryption for you though. +#[derive(Component)] +pub struct RawConnection { + reader: RawConnectionReader, + writer: RawConnectionWriter, + + /// Packets sent to this will be sent to the server. + + /// A task that reads packets from the server. The client is disconnected + /// when this task ends. + read_packets_task: tokio::task::JoinHandle<()>, + /// A task that writes packets from the server. + write_packets_task: tokio::task::JoinHandle<()>, + + connection_protocol: ConnectionProtocol, +} + +#[derive(Clone)] +struct RawConnectionReader { + pub incoming_packet_queue: Arc>>>, + pub run_schedule_sender: mpsc::UnboundedSender<()>, +} +#[derive(Clone)] +struct RawConnectionWriter { + pub outgoing_packets_sender: mpsc::UnboundedSender>, +} + +#[derive(Error, Debug)] +pub enum WritePacketError { + #[error("Wrong protocol state: expected {expected:?}, got {got:?}")] + WrongState { + expected: ConnectionProtocol, + got: ConnectionProtocol, + }, + #[error(transparent)] + Encoding(#[from] azalea_protocol::write::PacketEncodeError), +} + +impl RawConnection { + pub fn new( + run_schedule_sender: mpsc::UnboundedSender<()>, + connection_protocol: ConnectionProtocol, + raw_read_connection: RawReadConnection, + raw_write_connection: RawWriteConnection, + ) -> Self { + let (outgoing_packets_sender, outgoing_packets_receiver) = mpsc::unbounded_channel(); + + let incoming_packet_queue = Arc::new(Mutex::new(Vec::new())); + + let reader = RawConnectionReader { + incoming_packet_queue: incoming_packet_queue.clone(), + run_schedule_sender, + }; + let writer = RawConnectionWriter { + outgoing_packets_sender, + }; + + let read_packets_task = tokio::spawn(reader.clone().read_task(raw_read_connection)); + let write_packets_task = tokio::spawn( + writer + .clone() + .write_task(raw_write_connection, outgoing_packets_receiver), + ); + + Self { + reader, + writer, + read_packets_task, + write_packets_task, + connection_protocol, + } + } + + pub fn write_raw_packet(&self, raw_packet: Vec) { + self.writer + .outgoing_packets_sender + .send(raw_packet) + .unwrap(); + } + + /// Write the packet with the given state to the server. + /// + /// # Errors + /// + /// Returns an error if the packet is not valid for the current state, or if + /// encoding it failed somehow (like it's too big or something). + pub fn write_packet( + &self, + packet: P, + ) -> Result<(), WritePacketError> { + let raw_packet = serialize_packet(&packet)?; + self.write_raw_packet(raw_packet); + Ok(()) + } + + /// Returns whether the connection is still alive. + pub fn is_alive(&self) -> bool { + !self.read_packets_task.is_finished() + } + + pub fn incoming_packet_queue(&self) -> Arc>>> { + self.reader.incoming_packet_queue.clone() + } + + pub fn set_state(&mut self, connection_protocol: ConnectionProtocol) { + self.connection_protocol = connection_protocol; + } +} + +impl RawConnectionReader { + /// Loop that reads from the connection and adds the packets to the queue + + /// runs the schedule. + pub async fn read_task(self, mut read_conn: RawReadConnection) { + loop { + match read_conn.read().await { + Ok(raw_packet) => { + self.incoming_packet_queue.lock().push(raw_packet); + // tell the client to run all the systems + self.run_schedule_sender.send(()).unwrap(); + } + Err(error) => { + if !matches!(*error, ReadPacketError::ConnectionClosed) { + error!("Error reading packet from Client: {error:?}"); + } + break; + } + } + } + } +} + +impl RawConnectionWriter { + /// Consume the [`ServerboundGamePacket`] queue and actually write the + /// packets to the server. It's like this so writing packets doesn't need to + /// be awaited. + pub async fn write_task( + self, + mut write_conn: RawWriteConnection, + mut outgoing_packets_receiver: mpsc::UnboundedReceiver>, + ) { + while let Some(raw_packet) = outgoing_packets_receiver.recv().await { + if let Err(err) = write_conn.write(&raw_packet).await { + error!("Disconnecting because we couldn't write a packet: {err}."); + break; + }; + } + // receiver is automatically closed when it's dropped + } +} + +impl Drop for RawConnection { + /// Stop every active task when this `RawConnection` is dropped. + fn drop(&mut self) { + self.read_packets_task.abort(); + self.write_packets_task.abort(); + } +} diff --git a/azalea-client/src/received_registries.rs b/azalea-client/src/received_registries.rs index 845527ae..024f5222 100644 --- a/azalea-client/src/received_registries.rs +++ b/azalea-client/src/received_registries.rs @@ -1,7 +1,28 @@ -use azalea_protocol::packets::game::clientbound_login_packet::registry::RegistryRoot; -use bevy_ecs::component::Component; -use derive_more::Deref; +use std::collections::HashMap; -/// The registries that the server sent us on login. -#[derive(Clone, Debug, Component, Deref)] -pub struct ReceivedRegistries(pub RegistryRoot); +use azalea_core::ResourceLocation; +use azalea_nbt::Nbt; +use azalea_protocol::packets::configuration::clientbound_registry_data_packet::registry::{ + DimensionTypeElement, RegistryType, +}; +use bevy_ecs::prelude::*; +use serde::de::DeserializeOwned; + +/// The registries that were sent to us during the configuration state. +#[derive(Default, Component, Clone)] +pub struct ReceivedRegistries { + pub registries: HashMap, +} + +impl ReceivedRegistries { + fn get(&self, name: &ResourceLocation) -> Option { + let nbt = self.registries.get(name)?; + serde_json::from_value(serde_json::to_value(nbt).ok()?).ok() + } + + /// Get the dimension type registry, or `None` if it doesn't exist. You + /// should do some type of error handling if this returns `None`. + pub fn dimension_type(&self) -> Option> { + self.get(&ResourceLocation::new("minecraft:dimension_type")) + } +} -- cgit v1.2.3