aboutsummaryrefslogtreecommitdiff
path: root/azalea-client/src/plugins/packet
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2025-02-22 21:45:26 -0600
committerGitHub <noreply@github.com>2025-02-22 21:45:26 -0600
commite21e1b97bf9337e9f4747cd1b545b1b3a03e2ce7 (patch)
treeadd6f8bfce40d0c07845d8aa4c9945a0b918444c /azalea-client/src/plugins/packet
parentf8130c3c92946d2293634ba4e252d6bc93026c3c (diff)
downloadazalea-drasl-e21e1b97bf9337e9f4747cd1b545b1b3a03e2ce7.tar.xz
Refactor azalea-client (#205)
* start organizing packet_handling more by moving packet handlers into their own functions * finish writing all the handler functions for packets * use macro for generating match statement for packet handler functions * fix set_entity_data * update config state to also use handler functions * organize az-client file structure by moving things into plugins directory * fix merge issues
Diffstat (limited to 'azalea-client/src/plugins/packet')
-rw-r--r--azalea-client/src/plugins/packet/config/events.rs90
-rw-r--r--azalea-client/src/plugins/packet/config/mod.rs223
-rw-r--r--azalea-client/src/plugins/packet/game/events.rs178
-rw-r--r--azalea-client/src/plugins/packet/game/mod.rs1583
-rw-r--r--azalea-client/src/plugins/packet/login.rs114
-rw-r--r--azalea-client/src/plugins/packet/mod.rs109
6 files changed, 2297 insertions, 0 deletions
diff --git a/azalea-client/src/plugins/packet/config/events.rs b/azalea-client/src/plugins/packet/config/events.rs
new file mode 100644
index 00000000..6b647d74
--- /dev/null
+++ b/azalea-client/src/plugins/packet/config/events.rs
@@ -0,0 +1,90 @@
+use std::io::Cursor;
+
+use azalea_protocol::{
+ packets::{
+ config::{ClientboundConfigPacket, ServerboundConfigPacket},
+ Packet,
+ },
+ read::deserialize_packet,
+};
+use bevy_ecs::prelude::*;
+use tracing::{debug, error};
+
+use crate::{raw_connection::RawConnection, InConfigState};
+
+#[derive(Event, Debug, Clone)]
+pub struct ReceiveConfigPacketEvent {
+ /// The client entity that received the packet.
+ pub entity: Entity,
+ /// The packet that was actually received.
+ pub packet: ClientboundConfigPacket,
+}
+
+/// An event for sending a packet to the server while we're in the
+/// `configuration` state.
+#[derive(Event)]
+pub struct SendConfigPacketEvent {
+ pub sent_by: Entity,
+ pub packet: ServerboundConfigPacket,
+}
+impl SendConfigPacketEvent {
+ pub fn new(sent_by: Entity, packet: impl Packet<ServerboundConfigPacket>) -> Self {
+ let packet = packet.into_variant();
+ Self { sent_by, packet }
+ }
+}
+
+pub fn handle_send_packet_event(
+ mut send_packet_events: EventReader<SendConfigPacketEvent>,
+ mut query: Query<(&mut RawConnection, Option<&InConfigState>)>,
+) {
+ for event in send_packet_events.read() {
+ if let Ok((raw_conn, in_configuration_state)) = query.get_mut(event.sent_by) {
+ if in_configuration_state.is_none() {
+ error!(
+ "Tried to send a configuration packet {:?} while not in configuration state",
+ event.packet
+ );
+ continue;
+ }
+ debug!("Sending packet: {:?}", event.packet);
+ if let Err(e) = raw_conn.write_packet(event.packet.clone()) {
+ error!("Failed to send packet: {e}");
+ }
+ }
+ }
+}
+
+pub fn send_packet_events(
+ query: Query<(Entity, &RawConnection), With<InConfigState>>,
+ mut packet_events: ResMut<Events<ReceiveConfigPacketEvent>>,
+) {
+ // we manually clear and send the events at the beginning of each update
+ // since otherwise it'd cause issues with events in process_packet_events
+ // running twice
+ packet_events.clear();
+ for (player_entity, raw_conn) in &query {
+ let packets_lock = raw_conn.incoming_packet_queue();
+ let mut packets = packets_lock.lock();
+ if !packets.is_empty() {
+ for raw_packet in packets.iter() {
+ let packet = match deserialize_packet::<ClientboundConfigPacket>(&mut Cursor::new(
+ raw_packet,
+ )) {
+ Ok(packet) => packet,
+ Err(err) => {
+ error!("failed to read packet: {err:?}");
+ debug!("packet bytes: {raw_packet:?}");
+ continue;
+ }
+ };
+ packet_events.send(ReceiveConfigPacketEvent {
+ entity: player_entity,
+ packet,
+ });
+ }
+ // clear the packets right after we read them
+ packets.clear();
+ }
+ }
+}
diff --git a/azalea-client/src/plugins/packet/config/mod.rs b/azalea-client/src/plugins/packet/config/mod.rs
new file mode 100644
index 00000000..5cb19b9d
--- /dev/null
+++ b/azalea-client/src/plugins/packet/config/mod.rs
@@ -0,0 +1,223 @@
+mod events;
+
+use azalea_protocol::packets::config::*;
+use azalea_protocol::packets::ConnectionProtocol;
+use bevy_ecs::prelude::*;
+use bevy_ecs::system::SystemState;
+pub use events::*;
+use tracing::{debug, warn};
+
+use super::as_system;
+use crate::client::InConfigState;
+use crate::disconnect::DisconnectEvent;
+use crate::packet::game::KeepAliveEvent;
+use crate::raw_connection::RawConnection;
+use crate::{declare_packet_handlers, InstanceHolder};
+
+pub fn process_packet_events(ecs: &mut World) {
+ let mut events_owned = Vec::new();
+ let mut system_state: SystemState<EventReader<ReceiveConfigPacketEvent>> =
+ SystemState::new(ecs);
+ let mut events = system_state.get_mut(ecs);
+ for ReceiveConfigPacketEvent {
+ entity: player_entity,
+ packet,
+ } in events.read()
+ {
+ // we do this so `ecs` isn't borrowed for the whole loop
+ events_owned.push((*player_entity, packet.clone()));
+ }
+ for (player_entity, packet) in events_owned {
+ let mut handler = ConfigPacketHandler {
+ player: player_entity,
+ ecs,
+ };
+
+ declare_packet_handlers!(
+ ClientboundConfigPacket,
+ packet,
+ handler,
+ [
+ cookie_request,
+ custom_payload,
+ disconnect,
+ finish_configuration,
+ keep_alive,
+ ping,
+ reset_chat,
+ registry_data,
+ resource_pack_pop,
+ resource_pack_push,
+ store_cookie,
+ transfer,
+ update_enabled_features,
+ update_tags,
+ select_known_packs,
+ custom_report_details,
+ server_links,
+ ]
+ );
+ }
+}
+
+pub struct ConfigPacketHandler<'a> {
+ pub ecs: &'a mut World,
+ pub player: Entity,
+}
+impl ConfigPacketHandler<'_> {
+ pub fn registry_data(&mut self, p: ClientboundRegistryData) {
+ as_system::<Query<&mut InstanceHolder>>(self.ecs, |mut query| {
+ let instance_holder = query.get_mut(self.player).unwrap();
+ let mut instance = instance_holder.instance.write();
+
+ // add the new registry data
+ instance.registries.append(p.registry_id, p.entries);
+ });
+ }
+
+ pub fn custom_payload(&mut self, p: ClientboundCustomPayload) {
+ debug!("Got custom payload packet {p:?}");
+ }
+
+ pub fn disconnect(&mut self, p: ClientboundDisconnect) {
+ warn!("Got disconnect packet {p:?}");
+ as_system::<EventWriter<_>>(self.ecs, |mut events| {
+ events.send(DisconnectEvent {
+ entity: self.player,
+ reason: Some(p.reason),
+ });
+ });
+ }
+
+ pub fn finish_configuration(&mut self, p: ClientboundFinishConfiguration) {
+ debug!("got FinishConfiguration packet: {p:?}");
+
+ as_system::<(Commands, Query<&mut RawConnection>)>(
+ self.ecs,
+ |(mut commands, mut query)| {
+ let mut raw_conn = query.get_mut(self.player).unwrap();
+
+ raw_conn
+ .write_packet(ServerboundFinishConfiguration)
+ .expect(
+ "we should be in the right state and encoding this packet shouldn't fail",
+ );
+ raw_conn.set_state(ConnectionProtocol::Game);
+
+ // these components are added now that we're going to be in the Game state
+ commands
+ .entity(self.player)
+ .remove::<InConfigState>()
+ .insert(crate::JoinedClientBundle::default());
+ },
+ );
+ }
+
+ pub fn keep_alive(&mut self, p: ClientboundKeepAlive) {
+ debug!(
+ "Got keep alive packet (in configuration) {p:?} for {:?}",
+ self.player
+ );
+
+ as_system::<(Query<&RawConnection>, EventWriter<_>)>(self.ecs, |(query, mut events)| {
+ let raw_conn = query.get(self.player).unwrap();
+
+ events.send(KeepAliveEvent {
+ entity: self.player,
+ id: p.id,
+ });
+ raw_conn
+ .write_packet(ServerboundKeepAlive { id: p.id })
+ .unwrap();
+ });
+ }
+
+ pub fn ping(&mut self, p: ClientboundPing) {
+ debug!("Got ping packet (in configuration) {p:?}");
+
+ as_system::<Query<&RawConnection>>(self.ecs, |query| {
+ let raw_conn = query.get(self.player).unwrap();
+
+ raw_conn.write_packet(ServerboundPong { id: p.id }).unwrap();
+ });
+ }
+
+ pub fn resource_pack_push(&mut self, p: ClientboundResourcePackPush) {
+ debug!("Got resource pack push packet {p:?}");
+
+ as_system::<Query<&RawConnection>>(self.ecs, |query| {
+ let raw_conn = query.get(self.player).unwrap();
+
+ // always accept resource pack
+ raw_conn
+ .write_packet(ServerboundResourcePack {
+ id: p.id,
+ action: s_resource_pack::Action::Accepted,
+ })
+ .unwrap();
+ });
+ }
+
+ pub fn resource_pack_pop(&mut self, p: ClientboundResourcePackPop) {
+ debug!("Got resource pack pop packet {p:?}");
+ }
+
+ pub fn update_enabled_features(&mut self, p: ClientboundUpdateEnabledFeatures) {
+ debug!("Got update enabled features packet {p:?}");
+ }
+
+ pub fn update_tags(&mut self, _p: ClientboundUpdateTags) {
+ debug!("Got update tags packet");
+ }
+
+ pub fn cookie_request(&mut self, p: ClientboundCookieRequest) {
+ debug!("Got cookie request packet {p:?}");
+
+ as_system::<Query<&RawConnection>>(self.ecs, |query| {
+ let raw_conn = query.get(self.player).unwrap();
+
+ raw_conn
+ .write_packet(ServerboundCookieResponse {
+ key: p.key,
+ // cookies aren't implemented
+ payload: None,
+ })
+ .unwrap();
+ });
+ }
+
+ pub fn reset_chat(&mut self, p: ClientboundResetChat) {
+ debug!("Got reset chat packet {p:?}");
+ }
+
+ pub fn store_cookie(&mut self, p: ClientboundStoreCookie) {
+ debug!("Got store cookie packet {p:?}");
+ }
+
+ pub fn transfer(&mut self, p: ClientboundTransfer) {
+ debug!("Got transfer packet {p:?}");
+ }
+
+ pub fn select_known_packs(&mut self, p: ClientboundSelectKnownPacks) {
+ debug!("Got select known packs packet {p:?}");
+
+ as_system::<Query<&RawConnection>>(self.ecs, |query| {
+ let raw_conn = query.get(self.player).unwrap();
+
+ // resource pack management isn't implemented
+ raw_conn
+ .write_packet(ServerboundSelectKnownPacks {
+ known_packs: vec![],
+ })
+ .unwrap();
+ });
+ }
+
+ pub fn server_links(&mut self, p: ClientboundServerLinks) {
+ debug!("Got server links packet {p:?}");
+ }
+
+ pub fn custom_report_details(&mut self, p: ClientboundCustomReportDetails) {
+ debug!("Got custom report details packet {p:?}");
+ }
+}
diff --git a/azalea-client/src/plugins/packet/game/events.rs b/azalea-client/src/plugins/packet/game/events.rs
new file mode 100644
index 00000000..19f2a571
--- /dev/null
+++ b/azalea-client/src/plugins/packet/game/events.rs
@@ -0,0 +1,178 @@
+use std::{
+ io::Cursor,
+ sync::{Arc, Weak},
+};
+
+use azalea_chat::FormattedText;
+use azalea_core::resource_location::ResourceLocation;
+use azalea_entity::LocalEntity;
+use azalea_protocol::{
+ packets::{
+ Packet,
+ game::{ClientboundGamePacket, ClientboundPlayerCombatKill, ServerboundGamePacket},
+ },
+ read::deserialize_packet,
+};
+use azalea_world::Instance;
+use bevy_ecs::prelude::*;
+use parking_lot::RwLock;
+use tracing::{debug, error};
+use uuid::Uuid;
+
+use crate::{PlayerInfo, raw_connection::RawConnection};
+
+/// An event that's sent when we receive a packet.
+/// ```
+/// # use azalea_client::packet::game::ReceivePacketEvent;
+/// # use azalea_protocol::packets::game::ClientboundGamePacket;
+/// # use bevy_ecs::event::EventReader;
+///
+/// fn handle_packets(mut events: EventReader<ReceivePacketEvent>) {
+/// for ReceivePacketEvent {
+/// entity,
+/// packet,
+/// } in events.read() {
+/// match packet.as_ref() {
+/// ClientboundGamePacket::LevelParticles(p) => {
+/// // ...
+/// }
+/// _ => {}
+/// }
+/// }
+/// }
+/// ```
+#[derive(Event, Debug, Clone)]
+pub struct ReceivePacketEvent {
+ /// The client entity that received the packet.
+ pub entity: Entity,
+ /// The packet that was actually received.
+ pub packet: Arc<ClientboundGamePacket>,
+}
+
+/// An event for sending a packet to the server while we're in the `game` state.
+#[derive(Event)]
+pub struct SendPacketEvent {
+ pub sent_by: Entity,
+ pub packet: ServerboundGamePacket,
+}
+impl SendPacketEvent {
+ pub fn new(sent_by: Entity, packet: impl Packet<ServerboundGamePacket>) -> Self {
+ let packet = packet.into_variant();
+ Self { sent_by, packet }
+ }
+}
+
+pub fn handle_outgoing_packets(
+ mut send_packet_events: EventReader<SendPacketEvent>,
+ mut query: Query<&mut RawConnection>,
+) {
+ for event in send_packet_events.read() {
+ if let Ok(raw_connection) = query.get_mut(event.sent_by) {
+ // debug!("Sending packet: {:?}", event.packet);
+ if let Err(e) = raw_connection.write_packet(event.packet.clone()) {
+ error!("Failed to send packet: {e}");
+ }
+ }
+ }
+}
+
+pub fn send_receivepacketevent(
+ query: Query<(Entity, &RawConnection), With<LocalEntity>>,
+ mut packet_events: ResMut<Events<ReceivePacketEvent>>,
+) {
+ // 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::<ClientboundGamePacket>(&mut Cursor::new(raw_packet))
+ {
+ Ok(packet) => packet,
+ Err(err) => {
+ error!("failed to read packet: {err:?}");
+ debug!("packet bytes: {raw_packet:?}");
+ continue;
+ }
+ };
+ packet_events.send(ReceivePacketEvent {
+ entity: player_entity,
+ packet: Arc::new(packet),
+ });
+ }
+ // clear the packets right after we read them
+ packets.clear();
+ }
+ }
+}
+
+/// A player joined the game (or more specifically, was added to the tab
+/// list of a local player).
+#[derive(Event, Debug, Clone)]
+pub struct AddPlayerEvent {
+ /// The local player entity that received this event.
+ pub entity: Entity,
+ pub info: PlayerInfo,
+}
+/// A player left the game (or maybe is still in the game and was just
+/// removed from the tab list of a local player).
+#[derive(Event, Debug, Clone)]
+pub struct RemovePlayerEvent {
+ /// The local player entity that received this event.
+ pub entity: Entity,
+ pub info: PlayerInfo,
+}
+/// A player was updated in the tab list of a local player (gamemode, display
+/// name, or latency changed).
+#[derive(Event, Debug, Clone)]
+pub struct UpdatePlayerEvent {
+ /// The local player entity that received this event.
+ pub entity: Entity,
+ pub info: PlayerInfo,
+}
+
+/// Event for when an entity dies. dies. If it's a local player and there's a
+/// reason in the death screen, the [`ClientboundPlayerCombatKill`] will
+/// be included.
+#[derive(Event, Debug, Clone)]
+pub struct DeathEvent {
+ pub entity: Entity,
+ pub packet: Option<ClientboundPlayerCombatKill>,
+}
+
+/// A KeepAlive packet is sent from the server to verify that the client is
+/// still connected.
+#[derive(Event, Debug, Clone)]
+pub struct KeepAliveEvent {
+ pub entity: Entity,
+ /// The ID of the keepalive. This is an arbitrary number, but vanilla
+ /// servers use the time to generate this.
+ pub id: u64,
+}
+
+#[derive(Event, Debug, Clone)]
+pub struct ResourcePackEvent {
+ pub entity: Entity,
+ /// The random ID for this request to download the resource pack. The packet
+ /// for replying to a resource pack push must contain the same ID.
+ pub id: Uuid,
+ pub url: String,
+ pub hash: String,
+ pub required: bool,
+ pub prompt: Option<FormattedText>,
+}
+
+/// 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<RwLock<Instance>>,
+}
diff --git a/azalea-client/src/plugins/packet/game/mod.rs b/azalea-client/src/plugins/packet/game/mod.rs
new file mode 100644
index 00000000..98f76d13
--- /dev/null
+++ b/azalea-client/src/plugins/packet/game/mod.rs
@@ -0,0 +1,1583 @@
+mod events;
+
+use std::{collections::HashSet, ops::Add, sync::Arc};
+
+use azalea_core::{
+ game_type::GameMode,
+ math,
+ position::{ChunkPos, Vec3},
+};
+use azalea_entity::{
+ Dead, EntityBundle, EntityKind, LastSentPosition, LoadedBy, LocalEntity, LookDirection,
+ Physics, Position, RelativeEntityUpdate,
+ indexing::{EntityIdIndex, EntityUuidIndex},
+ metadata::{Health, apply_metadata},
+};
+use azalea_protocol::packets::game::*;
+use azalea_world::{InstanceContainer, InstanceName, MinecraftEntityId, PartialInstance};
+use bevy_ecs::{prelude::*, system::SystemState};
+pub use events::*;
+use tracing::{debug, error, trace, warn};
+
+use crate::{
+ ClientInformation, PlayerInfo,
+ chat::{ChatPacket, ChatReceivedEvent},
+ chunks, declare_packet_handlers,
+ disconnect::DisconnectEvent,
+ inventory::{
+ ClientSideCloseContainerEvent, Inventory, MenuOpenedEvent, SetContainerContentEvent,
+ },
+ local_player::{
+ GameProfileComponent, Hunger, InstanceHolder, LocalGameMode, PlayerAbilities, TabList,
+ },
+ movement::{KnockbackEvent, KnockbackType},
+ packet::as_system,
+};
+
+pub fn process_packet_events(ecs: &mut World) {
+ let mut events_owned = Vec::<(Entity, Arc<ClientboundGamePacket>)>::new();
+
+ {
+ let mut system_state = SystemState::<EventReader<ReceivePacketEvent>>::new(ecs);
+ let mut events = system_state.get_mut(ecs);
+ for ReceivePacketEvent {
+ entity: player_entity,
+ packet,
+ } in events.read()
+ {
+ // we do this so `ecs` isn't borrowed for the whole loop
+ events_owned.push((*player_entity, packet.clone()));
+ }
+ }
+
+ for (player_entity, packet) in events_owned {
+ let mut handler = GamePacketHandler {
+ player: player_entity,
+ ecs,
+ };
+
+ declare_packet_handlers!(
+ ClientboundGamePacket,
+ packet.as_ref(),
+ handler,
+ [
+ login,
+ set_chunk_cache_radius,
+ chunk_batch_start,
+ chunk_batch_finished,
+ custom_payload,
+ change_difficulty,
+ commands,
+ player_abilities,
+ set_cursor_item,
+ update_tags,
+ disconnect,
+ update_recipes,
+ entity_event,
+ player_position,
+ player_info_update,
+ player_info_remove,
+ set_chunk_cache_center,
+ chunks_biomes,
+ light_update,
+ level_chunk_with_light,
+ add_entity,
+ set_entity_data,
+ update_attributes,
+ set_entity_motion,
+ set_entity_link,
+ initialize_border,
+ set_time,
+ set_default_spawn_position,
+ set_health,
+ set_experience,
+ teleport_entity,
+ update_advancements,
+ rotate_head,
+ move_entity_pos,
+ move_entity_pos_rot,
+ move_entity_rot,
+ keep_alive,
+ remove_entities,
+ player_chat,
+ system_chat,
+ disguised_chat,
+ sound,
+ level_event,
+ block_update,
+ animate,
+ section_blocks_update,
+ game_event,
+ level_particles,
+ server_data,
+ set_equipment,
+ update_mob_effect,
+ add_experience_orb,
+ award_stats,
+ block_changed_ack,
+ block_destruction,
+ block_entity_data,
+ block_event,
+ boss_event,
+ command_suggestions,
+ container_set_content,
+ container_set_data,
+ container_set_slot,
+ container_close,
+ cooldown,
+ custom_chat_completions,
+ delete_chat,
+ explode,
+ forget_level_chunk,
+ horse_screen_open,
+ map_item_data,
+ merchant_offers,
+ move_vehicle,
+ open_book,
+ open_screen,
+ open_sign_editor,
+ ping,
+ place_ghost_recipe,
+ player_combat_end,
+ player_combat_enter,
+ player_combat_kill,
+ player_look_at,
+ remove_mob_effect,
+ resource_pack_push,
+ resource_pack_pop,
+ respawn,
+ start_configuration,
+ entity_position_sync,
+ select_advancements_tab,
+ set_action_bar_text,
+ set_border_center,
+ set_border_lerp_size,
+ set_border_size,
+ set_border_warning_delay,
+ set_border_warning_distance,
+ set_camera,
+ set_display_objective,
+ set_objective,
+ set_passengers,
+ set_player_team,
+ set_score,
+ set_simulation_distance,
+ set_subtitle_text,
+ set_title_text,
+ set_titles_animation,
+ clear_titles,
+ sound_entity,
+ stop_sound,
+ tab_list,
+ tag_query,
+ take_item_entity,
+ bundle_delimiter,
+ damage_event,
+ hurt_animation,
+ ticking_state,
+ ticking_step,
+ reset_score,
+ cookie_request,
+ debug_sample,
+ pong_response,
+ store_cookie,
+ transfer,
+ move_minecart_along_track,
+ set_held_slot,
+ set_player_inventory,
+ projectile_power,
+ custom_report_details,
+ server_links,
+ player_rotation,
+ recipe_book_add,
+ recipe_book_remove,
+ recipe_book_settings,
+ ]
+ );
+ }
+}
+
+pub struct GamePacketHandler<'a> {
+ pub ecs: &'a mut World,
+ pub player: Entity,
+}
+impl GamePacketHandler<'_> {
+ pub fn login(&mut self, p: &ClientboundLogin) {
+ debug!("Got login packet");
+
+ as_system::<(
+ Commands,
+ Query<(
+ &GameProfileComponent,
+ &ClientInformation,
+ Option<&mut InstanceName>,
+ Option<&mut LoadedBy>,
+ &mut EntityIdIndex,
+ &mut InstanceHolder,
+ )>,
+ EventWriter<InstanceLoadedEvent>,
+ ResMut<InstanceContainer>,
+ ResMut<EntityUuidIndex>,
+ EventWriter<SendPacketEvent>,
+ )>(
+ self.ecs,
+ |(
+ mut commands,
+ mut query,
+ mut instance_loaded_events,
+ mut instance_container,
+ mut entity_uuid_index,
+ mut send_packet_events,
+ )| {
+ let (
+ game_profile,
+ client_information,
+ instance_name,
+ loaded_by,
+ mut entity_id_index,
+ mut instance_holder,
+ ) = query.get_mut(self.player).unwrap();
+
+ let new_instance_name = p.common.dimension.clone();
+
+ if let Some(mut instance_name) = instance_name {
+ *instance_name = instance_name.clone();
+ } else {
+ commands
+ .entity(self.player)
+ .insert(InstanceName(new_instance_name.clone()));
+ }
+
+ let Some((_dimension_type, dimension_data)) = p
+ .common
+ .dimension_type(&instance_holder.instance.read().registries)
+ else {
+ return;
+ };
+
+ // add this world to the instance_container (or don't if it's already
+ // there)
+ let weak_instance = instance_container.insert(
+ new_instance_name.clone(),
+ dimension_data.height,
+ dimension_data.min_y,
+ &instance_holder.instance.read().registries,
+ );
+ instance_loaded_events.send(InstanceLoadedEvent {
+ entity: self.player,
+ name: new_instance_name.clone(),
+ instance: Arc::downgrade(&weak_instance),
+ });
+
+ // set the partial_world to an empty world
+ // (when we add chunks or entities those will be in the
+ // instance_container)
+
+ *instance_holder.partial_instance.write() = PartialInstance::new(
+ azalea_world::chunk_storage::calculate_chunk_storage_range(
+ client_information.view_distance.into(),
+ ),
+ // this argument makes it so other clients don't update this player entity
+ // in a shared instance
+ Some(self.player),
+ );
+ {
+ let map = instance_holder.instance.read().registries.map.clone();
+ let new_registries = &mut weak_instance.write().registries;
+ // add the registries from this instance to the weak instance
+ for (registry_name, registry) in map {
+ new_registries.map.insert(registry_name, registry);
+ }
+ }
+ instance_holder.instance = weak_instance;
+
+ let entity_bundle = EntityBundle::new(
+ game_profile.uuid,
+ Vec3::default(),
+ azalea_registry::EntityKind::Player,
+ new_instance_name,
+ );
+ let entity_id = p.player_id;
+ // insert our components into the ecs :)
+ commands.entity(self.player).insert((
+ entity_id,
+ LocalGameMode {
+ current: p.common.game_type,
+ previous: p.common.previous_game_type.into(),
+ },
+ entity_bundle,
+ ));
+
+ azalea_entity::indexing::add_entity_to_indexes(
+ entity_id,
+ self.player,
+ Some(game_profile.uuid),
+ &mut entity_id_index,
+ &mut entity_uuid_index,
+ &mut instance_holder.instance.write(),
+ );
+
+ // update or insert loaded_by
+ if let Some(mut loaded_by) = loaded_by {
+ loaded_by.insert(self.player);
+ } else {
+ commands
+ .entity(self.player)
+ .insert(LoadedBy(HashSet::from_iter(vec![self.player])));
+ }
+
+ // send the client information that we have set
+ debug!(
+ "Sending client information because login: {:?}",
+ client_information
+ );
+ send_packet_events.send(SendPacketEvent::new(self.player,
+ azalea_protocol::packets::game::s_client_information::ServerboundClientInformation { information: client_information.clone() },
+ ));
+ },
+ );
+ }
+
+ pub fn set_chunk_cache_radius(&mut self, p: &ClientboundSetChunkCacheRadius) {
+ debug!("Got set chunk cache radius packet {p:?}");
+ }
+
+ pub fn chunk_batch_start(&mut self, _p: &ClientboundChunkBatchStart) {
+ // the packet is empty, it's just a marker to tell us when the batch starts and
+ // ends
+ debug!("Got chunk batch start");
+
+ as_system::<EventWriter<_>>(self.ecs, |mut events| {
+ events.send(chunks::ChunkBatchStartEvent {
+ entity: self.player,
+ });
+ });
+ }
+
+ pub fn chunk_batch_finished(&mut self, p: &ClientboundChunkBatchFinished) {
+ debug!("Got chunk batch finished {p:?}");
+
+ as_system::<EventWriter<_>>(self.ecs, |mut events| {
+ events.send(chunks::ChunkBatchFinishedEvent {
+ entity: self.player,
+ batch_size: p.batch_size,
+ });
+ });
+ }
+
+ pub fn custom_payload(&mut self, p: &ClientboundCustomPayload) {
+ debug!("Got custom payload packet {p:?}");
+ }
+
+ pub fn change_difficulty(&mut self, p: &ClientboundChangeDifficulty) {
+ debug!("Got difficulty packet {p:?}");
+ }
+
+ pub fn commands(&mut self, _p: &ClientboundCommands) {
+ debug!("Got declare commands packet");
+ }
+
+ pub fn player_abilities(&mut self, p: &ClientboundPlayerAbilities) {
+ debug!("Got player abilities packet {p:?}");
+
+ as_system::<Query<&mut PlayerAbilities>>(self.ecs, |mut query| {
+ let mut player_abilities = query.get_mut(self.player).unwrap();
+
+ *player_abilities = PlayerAbilities::from(p);
+ });
+ }
+
+ pub fn set_cursor_item(&mut self, p: &ClientboundSetCursorItem) {
+ debug!("Got set cursor item packet {p:?}");
+ }
+
+ pub fn update_tags(&mut self, _p: &ClientboundUpdateTags) {
+ debug!("Got update tags packet");
+ }
+
+ pub fn disconnect(&mut self, p: &ClientboundDisconnect) {
+ warn!("Got disconnect packet {p:?}");
+
+ as_system::<EventWriter<_>>(self.ecs, |mut events| {
+ events.send(DisconnectEvent {
+ entity: self.player,
+ reason: Some(p.reason.clone()),
+ });
+ });
+ }
+
+ pub fn update_recipes(&mut self, _p: &ClientboundUpdateRecipes) {
+ debug!("Got update recipes packet");
+ }
+
+ pub fn entity_event(&mut self, _p: &ClientboundEntityEvent) {
+ // debug!("Got entity event packet {p:?}");
+ }
+
+ pub fn player_position(&mut self, p: &ClientboundPlayerPosition) {
+ debug!("Got player position packet {p:?}");
+
+ as_system::<(
+ Query<(
+ &mut Physics,
+ &mut LookDirection,
+ &mut Position,
+ &mut LastSentPosition,
+ )>,
+ EventWriter<SendPacketEvent>,
+ )>(self.ecs, |(mut query, mut send_packet_events)| {
+ let Ok((mut physics, mut direction, mut position, mut last_sent_position)) =
+ query.get_mut(self.player)
+ else {
+ return;
+ };
+
+ **last_sent_position = **position;
+
+ fn apply_change<T: Add<Output = T>>(base: T, condition: bool, change: T) -> T {
+ if condition { base + change } else { change }
+ }
+
+ let new_x = apply_change(position.x, p.relative.x, p.change.pos.x);
+ let new_y = apply_change(position.y, p.relative.y, p.change.pos.y);
+ let new_z = apply_change(position.z, p.relative.z, p.change.pos.z);
+
+ let new_y_rot = apply_change(
+ direction.y_rot,
+ p.relative.y_rot,
+ p.change.look_direction.y_rot,
+ );
+ let new_x_rot = apply_change(
+ direction.x_rot,
+ p.relative.x_rot,
+ p.change.look_direction.x_rot,
+ );
+
+ let mut new_delta_from_rotations = physics.velocity;
+ if p.relative.rotate_delta {
+ let y_rot_delta = direction.y_rot - new_y_rot;
+ let x_rot_delta = direction.x_rot - new_x_rot;
+ new_delta_from_rotations = new_delta_from_rotations
+ .x_rot(math::to_radians(x_rot_delta as f64) as f32)
+ .y_rot(math::to_radians(y_rot_delta as f64) as f32);
+ }
+
+ let new_delta = Vec3::new(
+ apply_change(
+ new_delta_from_rotations.x,
+ p.relative.delta_x,
+ p.change.delta.x,
+ ),
+ apply_change(
+ new_delta_from_rotations.y,
+ p.relative.delta_y,
+ p.change.delta.y,
+ ),
+ apply_change(
+ new_delta_from_rotations.z,
+ p.relative.delta_z,
+ p.change.delta.z,
+ ),
+ );
+
+ // apply the updates
+
+ physics.velocity = new_delta;
+
+ (direction.y_rot, direction.x_rot) = (new_y_rot, new_x_rot);
+
+ let new_pos = Vec3::new(new_x, new_y, new_z);
+ if new_pos != **position {
+ **position = new_pos;
+ }
+
+ // old_pos is set to the current position when we're teleported
+ physics.set_old_pos(&position);
+
+ // send the relevant packets
+
+ send_packet_events.send(SendPacketEvent::new(
+ self.player,
+ ServerboundAcceptTeleportation { id: p.id },
+ ));
+ send_packet_events.send(SendPacketEvent::new(
+ self.player,
+ ServerboundMovePlayerPosRot {
+ pos: new_pos,
+ look_direction: LookDirection::new(new_y_rot, new_x_rot),
+ // this is always false
+ on_ground: false,
+ },
+ ));
+ });
+ }
+
+ pub fn player_info_update(&mut self, p: &ClientboundPlayerInfoUpdate) {
+ debug!("Got player info packet {p:?}");
+
+ as_system::<(
+ Query<&mut TabList>,
+ EventWriter<AddPlayerEvent>,
+ EventWriter<UpdatePlayerEvent>,
+ ResMut<TabList>,
+ )>(
+ self.ecs,
+ |(
+ mut query,
+ mut add_player_events,
+ mut update_player_events,
+ mut tab_list_resource,
+ )| {
+ let mut tab_list = query.get_mut(self.player).unwrap();
+
+ for updated_info in &p.entries {
+ // add the new player maybe
+ if p.actions.add_player {
+ let info = PlayerInfo {
+ profile: updated_info.profile.clone(),
+ uuid: updated_info.profile.uuid,
+ gamemode: updated_info.game_mode,
+ latency: updated_info.latency,
+ display_name: updated_info.display_name.clone(),
+ };
+ tab_list.insert(updated_info.profile.uuid, info.clone());
+ add_player_events.send(AddPlayerEvent {
+ entity: self.player,
+ info: info.clone(),
+ });
+ } else if let Some(info) = tab_list.get_mut(&updated_info.profile.uuid) {
+ // `else if` because the block for add_player above
+ // already sets all the fields
+ if p.actions.update_game_mode {
+ info.gamemode = updated_info.game_mode;
+ }
+ if p.actions.update_latency {
+ info.latency = updated_info.latency;
+ }
+ if p.actions.update_display_name {
+ info.display_name.clone_from(&updated_info.display_name);
+ }
+ update_player_events.send(UpdatePlayerEvent {
+ entity: self.player,
+ info: info.clone(),
+ });
+ } else {
+ let uuid = updated_info.profile.uuid;
+ debug!("Ignoring PlayerInfoUpdate for unknown player {uuid}");
+ }
+ }
+
+ *tab_list_resource = tab_list.clone();
+ },
+ );
+ }
+
+ pub fn player_info_remove(&mut self, p: &ClientboundPlayerInfoRemove) {
+ debug!("Got chunk cache center packet {p:?}");
+
+ as_system::<(
+ Query<&mut TabList>,
+ EventWriter<RemovePlayerEvent>,
+ ResMut<TabList>,
+ )>(
+ self.ecs,
+ |(mut query, mut remove_player_events, mut tab_list_resource)| {
+ let mut tab_list = query.get_mut(self.player).unwrap();
+
+ for uuid in &p.profile_ids {
+ if let Some(info) = tab_list.remove(uuid) {
+ remove_player_events.send(RemovePlayerEvent {
+ entity: self.player,
+ info,
+ });
+ }
+ tab_list_resource.remove(uuid);
+ }
+ },
+ );
+ }
+
+ pub fn set_chunk_cache_center(&mut self, p: &ClientboundSetChunkCacheCenter) {
+ debug!("Got chunk cache center packet {p:?}");
+
+ as_system::<Query<&mut InstanceHolder>>(self.ecs, |mut query| {
+ let instance_holder = query.get_mut(self.player).unwrap();
+ let mut partial_world = instance_holder.partial_instance.write();
+
+ partial_world
+ .chunks
+ .update_view_center(ChunkPos::new(p.x, p.z));
+ });
+ }
+
+ pub fn chunks_biomes(&mut self, _p: &ClientboundChunksBiomes) {}
+
+ pub fn light_update(&mut self, _p: &ClientboundLightUpdate) {
+ // debug!("Got light update packet {p:?}");
+ }
+
+ pub fn level_chunk_with_light(&mut self, p: &ClientboundLevelChunkWithLight) {
+ debug!("Got chunk with light packet {} {}", p.x, p.z);
+
+ as_system::<EventWriter<_>>(self.ecs, |mut events| {
+ events.send(chunks::ReceiveChunkEvent {
+ entity: self.player,
+ packet: p.clone(),
+ });
+ });
+ }
+
+ pub fn add_entity(&mut self, p: &ClientboundAddEntity) {
+ debug!("Got add entity packet {p:?}");
+
+ as_system::<(
+ Commands,
+ Query<(&mut EntityIdIndex, Option<&InstanceName>, Option<&TabList>)>,
+ Query<&mut LoadedBy>,
+ Query<Entity>,
+ Res<InstanceContainer>,
+ ResMut<EntityUuidIndex>,
+ )>(
+ self.ecs,
+ |(
+ mut commands,
+ mut query,
+ mut loaded_by_query,
+ entity_query,
+ instance_container,
+ mut entity_uuid_index,
+ )| {
+ let (mut entity_id_index, instance_name, tab_list) =
+ query.get_mut(self.player).unwrap();
+
+ let entity_id = p.id;
+
+ let Some(instance_name) = instance_name else {
+ warn!("got add player packet but we haven't gotten a login packet yet");
+ return;
+ };
+
+ // check if the entity already exists, and if it does then only add to LoadedBy
+ let instance = instance_container.get(instance_name).unwrap();
+ if let Some(&ecs_entity) = instance.read().entity_by_id.get(&entity_id) {
+ // entity already exists
+ let Ok(mut loaded_by) = loaded_by_query.get_mut(ecs_entity) else {
+ // LoadedBy for this entity isn't in the ecs! figure out what went wrong
+ // and print an error
+
+ let entity_in_ecs = entity_query.get(ecs_entity).is_ok();
+
+ if entity_in_ecs {
+ error!(
+ "LoadedBy for entity {entity_id:?} ({ecs_entity:?}) isn't in the ecs, but the entity is in entity_by_id"
+ );
+ } else {
+ error!(
+ "Entity {entity_id:?} ({ecs_entity:?}) isn't in the ecs, but the entity is in entity_by_id"
+ );
+ }
+ return;
+ };
+ loaded_by.insert(self.player);
+
+ // per-client id index
+ entity_id_index.insert(entity_id, ecs_entity);
+
+ debug!("added to LoadedBy of entity {ecs_entity:?} with id {entity_id:?}");
+ return;
+ };
+
+ // entity doesn't exist in the global index!
+
+ let bundle = p.as_entity_bundle((**instance_name).clone());
+ let mut spawned =
+ commands.spawn((entity_id, LoadedBy(HashSet::from([self.player])), bundle));
+ let ecs_entity: Entity = spawned.id();
+ debug!("spawned entity {ecs_entity:?} with id {entity_id:?}");
+
+ azalea_entity::indexing::add_entity_to_indexes(
+ entity_id,
+ ecs_entity,
+ Some(p.uuid),
+ &mut entity_id_index,
+ &mut entity_uuid_index,
+ &mut instance.write(),
+ );
+
+ // add the GameProfileComponent if the uuid is in the tab list
+ if let Some(tab_list) = tab_list {
+ // (technically this makes it possible for non-player entities to have
+ // GameProfileComponents but the server would have to be doing something
+ // really weird)
+ if let Some(player_info) = tab_list.get(&p.uuid) {
+ spawned.insert(GameProfileComponent(player_info.profile.clone()));
+ }
+ }
+
+ // the bundle doesn't include the default entity metadata so we add that
+ // separately
+ p.apply_metadata(&mut spawned);
+ },
+ );
+ }
+
+ pub fn set_entity_data(&mut self, p: &ClientboundSetEntityData) {
+ as_system::<(
+ Commands,
+ Query<(&EntityIdIndex, &InstanceHolder)>,
+ // this is a separate query since it's applied on the entity id that's being updated
+ // instead of the player that received the packet
+ Query<&EntityKind>,
+ )>(self.ecs, |(mut commands, query, entity_kind_query)| {
+ let (entity_id_index, instance_holder) = query.get(self.player).unwrap();
+
+ let entity = entity_id_index.get(p.id);
+
+ let Some(entity) = entity else {
+ // some servers like hypixel trigger this a lot :(
+ debug!(
+ "Server sent an entity data packet for an entity id ({}) that we don't know about",
+ p.id
+ );
+ return;
+ };
+
+ let entity_kind = *entity_kind_query
+ .get(entity)
+ .expect("EntityKind component should always be present for entities");
+
+ debug!("Got set entity data packet {p:?} for entity of kind {entity_kind:?}");
+
+ let packed_items = p.packed_items.clone().to_vec();
+
+ // we use RelativeEntityUpdate because it makes sure changes aren't made
+ // multiple times
+ commands.entity(entity).queue(RelativeEntityUpdate {
+ partial_world: instance_holder.partial_instance.clone(),
+ update: Box::new(move |entity| {
+ let entity_id = entity.id();
+ entity.world_scope(|world| {
+ let mut commands_system_state = SystemState::<Commands>::new(world);
+ let mut commands = commands_system_state.get_mut(world);
+ let mut entity_commands = commands.entity(entity_id);
+ if let Err(e) =
+ apply_metadata(&mut entity_commands, *entity_kind, packed_items)
+ {
+ warn!("{e}");
+ }
+ commands_system_state.apply(world);
+ });
+ }),
+ });
+ });
+ }
+
+ pub fn update_attributes(&mut self, _p: &ClientboundUpdateAttributes) {
+ // debug!("Got update attributes packet {p:?}");
+ }
+
+ pub fn set_entity_motion(&mut self, p: &ClientboundSetEntityMotion) {
+ // vanilla servers use this packet for knockback, but note that the Explode
+ // packet is also sometimes used by servers for knockback
+
+ as_system::<(Commands, Query<(&EntityIdIndex, &InstanceHolder)>)>(
+ self.ecs,
+ |(mut commands, query)| {
+ let (entity_id_index, instance_holder) = query.get(self.player).unwrap();
+
+ let Some(entity) = entity_id_index.get(p.id) else {
+ // note that this log (and some other ones like the one in RemoveEntities)
+ // sometimes happens when killing mobs. it seems to be a vanilla bug, which is
+ // why it's a debug log instead of a warning
+ debug!(
+ "Got set entity motion packet for unknown entity id {}",
+ p.id
+ );
+ return;
+ };
+
+ // this is to make sure the same entity velocity update doesn't get sent
+ // multiple times when in swarms
+
+ let knockback = KnockbackType::Set(Vec3 {
+ x: p.delta.xa as f64 / 8000.,
+ y: p.delta.ya as f64 / 8000.,
+ z: p.delta.za as f64 / 8000.,
+ });
+
+ commands.entity(entity).queue(RelativeEntityUpdate {
+ partial_world: instance_holder.partial_instance.clone(),
+ update: Box::new(move |entity_mut| {
+ entity_mut.world_scope(|world| {
+ world.send_event(KnockbackEvent { entity, knockback })
+ });
+ }),
+ });
+ },
+ );
+ }
+
+ pub fn set_entity_link(&mut self, p: &ClientboundSetEntityLink) {
+ debug!("Got set entity link packet {p:?}");
+ }
+
+ pub fn initialize_border(&mut self, p: &ClientboundInitializeBorder) {
+ debug!("Got initialize border packet {p:?}");
+ }
+
+ pub fn set_time(&mut self, _p: &ClientboundSetTime) {
+ // debug!("Got set time packet {p:?}");
+ }
+
+ pub fn set_default_spawn_position(&mut self, p: &ClientboundSetDefaultSpawnPosition) {
+ debug!("Got set default spawn position packet {p:?}");
+ }
+
+ pub fn set_health(&mut self, p: &ClientboundSetHealth) {
+ debug!("Got set health packet {p:?}");
+
+ as_system::<Query<(&mut Health, &mut Hunger)>>(self.ecs, |mut query| {
+ let (mut health, mut hunger) = query.get_mut(self.player).unwrap();
+
+ **health = p.health;
+ (hunger.food, hunger.saturation) = (p.food, p.saturation);
+
+ // the `Dead` component is added by the `update_dead` system
+ // in azalea-world and then the `dead_event` system fires
+ // the Death event.
+ });
+ }
+
+ pub fn set_experience(&mut self, p: &ClientboundSetExperience) {
+ debug!("Got set experience packet {p:?}");
+ }
+
+ pub fn teleport_entity(&mut self, p: &ClientboundTeleportEntity) {
+ as_system::<(Commands, Query<(&EntityIdIndex, &InstanceHolder)>)>(
+ self.ecs,
+ |(mut commands, mut query)| {
+ let (entity_id_index, instance_holder) = query.get_mut(self.player).unwrap();
+
+ let Some(entity) = entity_id_index.get(p.id) else {
+ warn!("Got teleport entity packet for unknown entity id {}", p.id);
+ return;
+ };
+
+ let new_pos = p.change.pos;
+ let new_look_direction = LookDirection {
+ x_rot: (p.change.look_direction.x_rot as i32 * 360) as f32 / 256.,
+ y_rot: (p.change.look_direction.y_rot as i32 * 360) as f32 / 256.,
+ };
+ commands.entity(entity).queue(RelativeEntityUpdate {
+ partial_world: instance_holder.partial_instance.clone(),
+ update: Box::new(move |entity| {
+ let mut position = entity.get_mut::<Position>().unwrap();
+ if new_pos != **position {
+ **position = new_pos;
+ }
+ let position = *position;
+ let mut look_direction = entity.get_mut::<LookDirection>().unwrap();
+ if new_look_direction != *look_direction {
+ *look_direction = new_look_direction;
+ }
+ // old_pos is set to the current position when we're teleported
+ let mut physics = entity.get_mut::<Physics>().unwrap();
+ physics.set_old_pos(&position);
+ }),
+ });
+ },
+ );
+ }
+
+ pub fn update_advancements(&mut self, p: &ClientboundUpdateAdvancements) {
+ debug!("Got update advancements packet {p:?}");
+ }
+
+ pub fn rotate_head(&mut self, _p: &ClientboundRotateHead) {}
+
+ pub fn move_entity_pos(&mut self, p: &ClientboundMoveEntityPos) {
+ as_system::<(Commands, Query<(&EntityIdIndex, &InstanceHolder)>)>(
+ self.ecs,
+ |(mut commands, mut query)| {
+ let (entity_id_index, instance_holder) = query.get_mut(self.player).unwrap();
+
+ debug!("Got move entity pos packet {p:?}");
+
+ let Some(entity) = entity_id_index.get(p.entity_id) else {
+ debug!(
+ "Got move entity pos packet for unknown entity id {}",
+ p.entity_id
+ );
+ return;
+ };
+
+ let new_delta = p.delta.clone();
+ let new_on_ground = p.on_ground;
+ commands.entity(entity).queue(RelativeEntityUpdate {
+ partial_world: instance_holder.partial_instance.clone(),
+ update: Box::new(move |entity_mut| {
+ let mut physics = entity_mut.get_mut::<Physics>().unwrap();
+ let new_pos = physics.vec_delta_codec.decode(
+ new_delta.xa as i64,
+ new_delta.ya as i64,
+ new_delta.za as i64,
+ );
+ physics.vec_delta_codec.set_base(new_pos);
+ physics.set_on_ground(new_on_ground);
+
+ let mut position = entity_mut.get_mut::<Position>().unwrap();
+ if new_pos != **position {
+ **position = new_pos;
+ }
+ }),
+ });
+ },
+ );
+ }
+
+ pub fn move_entity_pos_rot(&mut self, p: &ClientboundMoveEntityPosRot) {
+ as_system::<(Commands, Query<(&EntityIdIndex, &InstanceHolder)>)>(
+ self.ecs,
+ |(mut commands, mut query)| {
+ let (entity_id_index, instance_holder) = query.get_mut(self.player).unwrap();
+
+ debug!("Got move entity pos rot packet {p:?}");
+
+ let entity = entity_id_index.get(p.entity_id);
+
+ let Some(entity) = entity else {
+ // often triggered by hypixel :(
+ debug!(
+ "Got move entity pos rot packet for unknown entity id {}",
+ p.entity_id
+ );
+ return;
+ };
+
+ let new_delta = p.delta.clone();
+ let new_look_direction = LookDirection {
+ x_rot: (p.x_rot as i32 * 360) as f32 / 256.,
+ y_rot: (p.y_rot as i32 * 360) as f32 / 256.,
+ };
+
+ let new_on_ground = p.on_ground;
+
+ commands.entity(entity).queue(RelativeEntityUpdate {
+ partial_world: instance_holder.partial_instance.clone(),
+ update: Box::new(move |entity_mut| {
+ let mut physics = entity_mut.get_mut::<Physics>().unwrap();
+ let new_pos = physics.vec_delta_codec.decode(
+ new_delta.xa as i64,
+ new_delta.ya as i64,
+ new_delta.za as i64,
+ );
+ physics.vec_delta_codec.set_base(new_pos);
+ physics.set_on_ground(new_on_ground);
+
+ let mut position = entity_mut.get_mut::<Position>().unwrap();
+ if new_pos != **position {
+ **position = new_pos;
+ }
+
+ let mut look_direction = entity_mut.get_mut::<LookDirection>().unwrap();
+ if new_look_direction != *look_direction {
+ *look_direction = new_look_direction;
+ }
+ }),
+ });
+ },
+ );
+ }
+
+ pub fn move_entity_rot(&mut self, p: &ClientboundMoveEntityRot) {
+ as_system::<(Commands, Query<(&EntityIdIndex, &InstanceHolder)>)>(
+ self.ecs,
+ |(mut commands, mut query)| {
+ let (entity_id_index, instance_holder) = query.get_mut(self.player).unwrap();
+
+ let entity = entity_id_index.get(p.entity_id);
+ if let Some(entity) = entity {
+ let new_look_direction = LookDirection {
+ x_rot: (p.x_rot as i32 * 360) as f32 / 256.,
+ y_rot: (p.y_rot as i32 * 360) as f32 / 256.,
+ };
+ let new_on_ground = p.on_ground;
+
+ commands.entity(entity).queue(RelativeEntityUpdate {
+ partial_world: instance_holder.partial_instance.clone(),
+ update: Box::new(move |entity_mut| {
+ let mut physics = entity_mut.get_mut::<Physics>().unwrap();
+ physics.set_on_ground(new_on_ground);
+
+ let mut look_direction = entity_mut.get_mut::<LookDirection>().unwrap();
+ if new_look_direction != *look_direction {
+ *look_direction = new_look_direction;
+ }
+ }),
+ });
+ } else {
+ warn!(
+ "Got move entity rot packet for unknown entity id {}",
+ p.entity_id
+ );
+ }
+ },
+ );
+ }
+ pub fn keep_alive(&mut self, p: &ClientboundKeepAlive) {
+ debug!("Got keep alive packet {p:?} for {:?}", self.player);
+
+ as_system::<(EventWriter<KeepAliveEvent>, EventWriter<SendPacketEvent>)>(
+ self.ecs,
+ |(mut keepalive_events, mut send_packet_events)| {
+ keepalive_events.send(KeepAliveEvent {
+ entity: self.player,
+ id: p.id,
+ });
+ send_packet_events.send(SendPacketEvent::new(
+ self.player,
+ ServerboundKeepAlive { id: p.id },
+ ));
+ },
+ );
+ }
+
+ pub fn remove_entities(&mut self, p: &ClientboundRemoveEntities) {
+ debug!("Got remove entities packet {p:?}");
+
+ as_system::<(Query<&mut EntityIdIndex>, Query<&mut LoadedBy>)>(
+ self.ecs,
+ |(mut query, mut entity_query)| {
+ let Ok(mut entity_id_index) = query.get_mut(self.player) else {
+ warn!("our local player doesn't have EntityIdIndex");
+ return;
+ };
+
+ for &id in &p.entity_ids {
+ let Some(entity) = entity_id_index.remove(id) else {
+ debug!(
+ "Tried to remove entity with id {id} but it wasn't in the EntityIdIndex"
+ );
+ continue;
+ };
+ let Ok(mut loaded_by) = entity_query.get_mut(entity) else {
+ warn!(
+ "tried to despawn entity {id} but it doesn't have a LoadedBy component",
+ );
+ continue;
+ };
+
+ // the [`remove_despawned_entities_from_indexes`] system will despawn the entity
+ // if it's not loaded by anything anymore
+
+ // also we can't just ecs.despawn because if we're in a swarm then the entity
+ // might still be loaded by another client
+
+ loaded_by.remove(&self.player);
+ }
+ },
+ );
+ }
+ pub fn player_chat(&mut self, p: &ClientboundPlayerChat) {
+ debug!("Got player chat packet {p:?}");
+
+ as_system::<EventWriter<_>>(self.ecs, |mut events| {
+ events.send(ChatReceivedEvent {
+ entity: self.player,
+ packet: ChatPacket::Player(Arc::new(p.clone())),
+ });
+ });
+ }
+
+ pub fn system_chat(&mut self, p: &ClientboundSystemChat) {
+ debug!("Got system chat packet {p:?}");
+
+ as_system::<EventWriter<_>>(self.ecs, |mut events| {
+ events.send(ChatReceivedEvent {
+ entity: self.player,
+ packet: ChatPacket::System(Arc::new(p.clone())),
+ });
+ });
+ }
+
+ pub fn disguised_chat(&mut self, p: &ClientboundDisguisedChat) {
+ debug!("Got disguised chat packet {p:?}");
+
+ as_system::<EventWriter<_>>(self.ecs, |mut events| {
+ events.send(ChatReceivedEvent {
+ entity: self.player,
+ packet: ChatPacket::Disguised(Arc::new(p.clone())),
+ });
+ });
+ }
+
+ pub fn sound(&mut self, _p: &ClientboundSound) {}
+
+ pub fn level_event(&mut self, p: &ClientboundLevelEvent) {
+ debug!("Got level event packet {p:?}");
+ }
+
+ pub fn block_update(&mut self, p: &ClientboundBlockUpdate) {
+ debug!("Got block update packet {p:?}");
+
+ as_system::<Query<&mut InstanceHolder>>(self.ecs, |mut query| {
+ let local_player = query.get_mut(self.player).unwrap();
+
+ let world = local_player.instance.write();
+
+ world.chunks.set_block_state(&p.pos, p.block_state);
+ });
+ }
+
+ pub fn animate(&mut self, p: &ClientboundAnimate) {
+ debug!("Got animate packet {p:?}");
+ }
+
+ pub fn section_blocks_update(&mut self, p: &ClientboundSectionBlocksUpdate) {
+ debug!("Got section blocks update packet {p:?}");
+
+ as_system::<Query<&mut InstanceHolder>>(self.ecs, |mut query| {
+ let local_player = query.get_mut(self.player).unwrap();
+ let world = local_player.instance.write();
+ for state in &p.states {
+ world
+ .chunks
+ .set_block_state(&(p.section_pos + state.pos), state.state);
+ }
+ });
+ }
+
+ pub fn game_event(&mut self, p: &ClientboundGameEvent) {
+ use azalea_protocol::packets::game::c_game_event::EventType;
+
+ debug!("Got game event packet {p:?}");
+
+ #[allow(clippy::single_match)]
+ match p.event {
+ EventType::ChangeGameMode => {
+ as_system::<Query<&mut LocalGameMode>>(self.ecs, |mut query| {
+ let mut local_game_mode = query.get_mut(self.player).unwrap();
+ if let Some(new_game_mode) = GameMode::from_id(p.param as u8) {
+ local_game_mode.current = new_game_mode;
+ }
+ });
+ }
+ _ => {}
+ }
+ }
+
+ pub fn level_particles(&mut self, p: &ClientboundLevelParticles) {
+ debug!("Got level particles packet {p:?}");
+ }
+
+ pub fn server_data(&mut self, p: &ClientboundServerData) {
+ debug!("Got server data packet {p:?}");
+ }
+
+ pub fn set_equipment(&mut self, p: &ClientboundSetEquipment) {
+ debug!("Got set equipment packet {p:?}");
+ }
+
+ pub fn update_mob_effect(&mut self, p: &ClientboundUpdateMobEffect) {
+ debug!("Got update mob effect packet {p:?}");
+ }
+
+ pub fn add_experience_orb(&mut self, _p: &ClientboundAddExperienceOrb) {}
+
+ pub fn award_stats(&mut self, _p: &ClientboundAwardStats) {}
+
+ pub fn block_changed_ack(&mut self, _p: &ClientboundBlockChangedAck) {}
+
+ pub fn block_destruction(&mut self, _p: &ClientboundBlockDestruction) {}
+
+ pub fn block_entity_data(&mut self, _p: &ClientboundBlockEntityData) {}
+
+ pub fn block_event(&mut self, p: &ClientboundBlockEvent) {
+ debug!("Got block event packet {p:?}");
+ }
+
+ pub fn boss_event(&mut self, _p: &ClientboundBossEvent) {}
+
+ pub fn command_suggestions(&mut self, _p: &ClientboundCommandSuggestions) {}
+
+ pub fn container_set_content(&mut self, p: &ClientboundContainerSetContent) {
+ debug!("Got container set content packet {p:?}");
+
+ as_system::<(Query<&mut Inventory>, EventWriter<_>)>(
+ self.ecs,
+ |(mut query, mut events)| {
+ let mut inventory = query.get_mut(self.player).unwrap();
+
+ // container id 0 is always the player's inventory
+ if p.container_id == 0 {
+ // this is just so it has the same type as the `else` block
+ for (i, slot) in p.items.iter().enumerate() {
+ if let Some(slot_mut) = inventory.inventory_menu.slot_mut(i) {
+ *slot_mut = slot.clone();
+ }
+ }
+ } else {
+ events.send(SetContainerContentEvent {
+ entity: self.player,
+ slots: p.items.clone(),
+ container_id: p.container_id,
+ });
+ }
+ },
+ );
+ }
+
+ pub fn container_set_data(&mut self, p: &ClientboundContainerSetData) {
+ debug!("Got container set data packet {p:?}");
+
+ // TODO: handle ContainerSetData packet
+ // this is used for various things like the furnace progress
+ // bar
+ // see https://wiki.vg/Protocol#Set_Container_Property
+
+ // as_system::<Query<&mut Inventory>>(self.ecs, |mut query| {
+ // let inventory = query.get_mut(self.player).unwrap();
+ // });
+ }
+
+ pub fn container_set_slot(&mut self, p: &ClientboundContainerSetSlot) {
+ debug!("Got container set slot packet {p:?}");
+
+ as_system::<Query<&mut Inventory>>(self.ecs, |mut query| {
+ let mut inventory = query.get_mut(self.player).unwrap();
+
+ if p.container_id == -1 {
+ // -1 means carried item
+ inventory.carried = p.item_stack.clone();
+ } else if p.container_id == -2 {
+ if let Some(slot) = inventory.inventory_menu.slot_mut(p.slot.into()) {
+ *slot = p.item_stack.clone();
+ }
+ } else {
+ let is_creative_mode_and_inventory_closed = false;
+ // technically minecraft has slightly different behavior here if you're in
+ // creative mode and have your inventory open
+ if p.container_id == 0 && azalea_inventory::Player::is_hotbar_slot(p.slot.into()) {
+ // minecraft also sets a "pop time" here which is used for an animation
+ // but that's not really necessary
+ if let Some(slot) = inventory.inventory_menu.slot_mut(p.slot.into()) {
+ *slot = p.item_stack.clone();
+ }
+ } else if p.container_id == inventory.id
+ && (p.container_id != 0 || !is_creative_mode_and_inventory_closed)
+ {
+ // var2.containerMenu.setItem(var4, var1.getStateId(), var3);
+ if let Some(slot) = inventory.menu_mut().slot_mut(p.slot.into()) {
+ *slot = p.item_stack.clone();
+ inventory.state_id = p.state_id;
+ }
+ }
+ }
+ });
+ }
+
+ pub fn container_close(&mut self, p: &ClientboundContainerClose) {
+ // there's a container_id field in the packet, but minecraft doesn't actually
+ // check it
+
+ debug!("Got container close packet {p:?}");
+
+ as_system::<EventWriter<_>>(self.ecs, |mut events| {
+ events.send(ClientSideCloseContainerEvent {
+ entity: self.player,
+ });
+ });
+ }
+
+ pub fn cooldown(&mut self, _p: &ClientboundCooldown) {}
+
+ pub fn custom_chat_completions(&mut self, _p: &ClientboundCustomChatCompletions) {}
+
+ pub fn delete_chat(&mut self, _p: &ClientboundDeleteChat) {}
+
+ pub fn explode(&mut self, p: &ClientboundExplode) {
+ trace!("Got explode packet {p:?}");
+
+ as_system::<EventWriter<_>>(self.ecs, |mut knockback_events| {
+ if let Some(knockback) = p.knockback {
+ knockback_events.send(KnockbackEvent {
+ entity: self.player,
+ knockback: KnockbackType::Set(knockback),
+ });
+ }
+ });
+ }
+
+ pub fn forget_level_chunk(&mut self, p: &ClientboundForgetLevelChunk) {
+ debug!("Got forget level chunk packet {p:?}");
+
+ as_system::<Query<&mut InstanceHolder>>(self.ecs, |mut query| {
+ let local_player = query.get_mut(self.player).unwrap();
+
+ let mut partial_instance = local_player.partial_instance.write();
+
+ partial_instance.chunks.limited_set(&p.pos, None);
+ });
+ }
+
+ pub fn horse_screen_open(&mut self, _p: &ClientboundHorseScreenOpen) {}
+
+ pub fn map_item_data(&mut self, _p: &ClientboundMapItemData) {}
+
+ pub fn merchant_offers(&mut self, _p: &ClientboundMerchantOffers) {}
+
+ pub fn move_vehicle(&mut self, _p: &ClientboundMoveVehicle) {}
+
+ pub fn open_book(&mut self, _p: &ClientboundOpenBook) {}
+
+ pub fn open_screen(&mut self, p: &ClientboundOpenScreen) {
+ debug!("Got open screen packet {p:?}");
+
+ as_system::<EventWriter<_>>(self.ecs, |mut events| {
+ events.send(MenuOpenedEvent {
+ entity: self.player,
+ window_id: p.container_id,
+ menu_type: p.menu_type,
+ title: p.title.to_owned(),
+ });
+ });
+ }
+
+ pub fn open_sign_editor(&mut self, _p: &ClientboundOpenSignEditor) {}
+
+ pub fn ping(&mut self, p: &ClientboundPing) {
+ debug!("Got ping packet {p:?}");
+
+ as_system::<EventWriter<_>>(self.ecs, |mut events| {
+ events.send(SendPacketEvent::new(
+ self.player,
+ ServerboundPong { id: p.id },
+ ));
+ });
+ }
+
+ pub fn place_ghost_recipe(&mut self, _p: &ClientboundPlaceGhostRecipe) {}
+
+ pub fn player_combat_end(&mut self, _p: &ClientboundPlayerCombatEnd) {}
+
+ pub fn player_combat_enter(&mut self, _p: &ClientboundPlayerCombatEnter) {}
+
+ pub fn player_combat_kill(&mut self, p: &ClientboundPlayerCombatKill) {
+ debug!("Got player kill packet {p:?}");
+
+ as_system::<(
+ Commands,
+ Query<(&MinecraftEntityId, Option<&Dead>)>,
+ EventWriter<_>,
+ )>(self.ecs, |(mut commands, mut query, mut events)| {
+ let (entity_id, dead) = query.get_mut(self.player).unwrap();
+
+ if *entity_id == p.player_id && dead.is_none() {
+ commands.entity(self.player).insert(Dead);
+ events.send(DeathEvent {
+ entity: self.player,
+ packet: Some(p.clone()),
+ });
+ }
+ });
+ }
+
+ pub fn player_look_at(&mut self, _p: &ClientboundPlayerLookAt) {}
+
+ pub fn remove_mob_effect(&mut self, _p: &ClientboundRemoveMobEffect) {}
+
+ pub fn resource_pack_push(&mut self, p: &ClientboundResourcePackPush) {
+ debug!("Got resource pack packet {p:?}");
+
+ as_system::<EventWriter<_>>(self.ecs, |mut events| {
+ events.send(ResourcePackEvent {
+ entity: self.player,
+ id: p.id,
+ url: p.url.to_owned(),
+ hash: p.hash.to_owned(),
+ required: p.required,
+ prompt: p.prompt.to_owned(),
+ });
+ });
+ }
+
+ pub fn resource_pack_pop(&mut self, _p: &ClientboundResourcePackPop) {}
+
+ pub fn respawn(&mut self, p: &ClientboundRespawn) {
+ debug!("Got respawn packet {p:?}");
+
+ as_system::<(
+ Commands,
+ Query<(
+ &mut InstanceHolder,
+ &GameProfileComponent,
+ &ClientInformation,
+ )>,
+ EventWriter<_>,
+ ResMut<InstanceContainer>,
+ )>(
+ self.ecs,
+ |(mut commands, mut query, mut events, mut instance_container)| {
+ let (mut instance_holder, game_profile, client_information) =
+ query.get_mut(self.player).unwrap();
+
+ let new_instance_name = p.common.dimension.clone();
+
+ let Some((_dimension_type, dimension_data)) = p
+ .common
+ .dimension_type(&instance_holder.instance.read().registries)
+ else {
+ return;
+ };
+
+ // add this world to the instance_container (or don't if it's already
+ // there)
+ let weak_instance = instance_container.insert(
+ new_instance_name.clone(),
+ dimension_data.height,
+ dimension_data.min_y,
+ &instance_holder.instance.read().registries,
+ );
+ events.send(InstanceLoadedEvent {
+ entity: self.player,
+ name: new_instance_name.clone(),
+ instance: Arc::downgrade(&weak_instance),
+ });
+
+ // set the partial_world to an empty world
+ // (when we add chunks or entities those will be in the
+ // instance_container)
+
+ *instance_holder.partial_instance.write() = PartialInstance::new(
+ azalea_world::chunk_storage::calculate_chunk_storage_range(
+ client_information.view_distance.into(),
+ ),
+ Some(self.player),
+ );
+ instance_holder.instance = weak_instance;
+
+ // this resets a bunch of our components like physics and stuff
+ let entity_bundle = EntityBundle::new(
+ game_profile.uuid,
+ Vec3::default(),
+ azalea_registry::EntityKind::Player,
+ new_instance_name,
+ );
+ // update the local gamemode and metadata things
+ commands.entity(self.player).insert((
+ LocalGameMode {
+ current: p.common.game_type,
+ previous: p.common.previous_game_type.into(),
+ },
+ entity_bundle,
+ ));
+
+ // Remove the Dead marker component from the player.
+ commands.entity(self.player).remove::<Dead>();
+ },
+ )
+ }
+
+ pub fn start_configuration(&mut self, _p: &ClientboundStartConfiguration) {
+ as_system::<(Commands, EventWriter<_>)>(self.ecs, |(mut commands, mut events)| {
+ events.send(SendPacketEvent::new(
+ self.player,
+ ServerboundConfigurationAcknowledged {},
+ ));
+
+ commands
+ .entity(self.player)
+ .insert(crate::client::InConfigState)
+ .remove::<crate::JoinedClientBundle>();
+ });
+ }
+
+ pub fn entity_position_sync(&mut self, p: &ClientboundEntityPositionSync) {
+ as_system::<(Commands, Query<(&EntityIdIndex, &InstanceHolder)>)>(
+ self.ecs,
+ |(mut commands, mut query)| {
+ let (entity_id_index, instance_holder) = query.get_mut(self.player).unwrap();
+
+ let Some(entity) = entity_id_index.get(p.id) else {
+ debug!("Got teleport entity packet for unknown entity id {}", p.id);
+ return;
+ };
+
+ let new_position = p.values.pos;
+ let new_on_ground = p.on_ground;
+ let new_look_direction = p.values.look_direction;
+
+ commands.entity(entity).queue(RelativeEntityUpdate {
+ partial_world: instance_holder.partial_instance.clone(),
+ update: Box::new(move |entity_mut| {
+ let is_local_entity = entity_mut.get::<LocalEntity>().is_some();
+ let mut physics = entity_mut.get_mut::<Physics>().unwrap();
+
+ physics.vec_delta_codec.set_base(new_position);
+
+ if is_local_entity {
+ debug!("Ignoring entity position sync packet for local player");
+ return;
+ }
+
+ physics.set_on_ground(new_on_ground);
+
+ let mut last_sent_position =
+ entity_mut.get_mut::<LastSentPosition>().unwrap();
+ **last_sent_position = new_position;
+ let mut position = entity_mut.get_mut::<Position>().unwrap();
+ **position = new_position;
+
+ let mut look_direction = entity_mut.get_mut::<LookDirection>().unwrap();
+ *look_direction = new_look_direction;
+ }),
+ });
+ },
+ );
+ }
+
+ pub fn select_advancements_tab(&mut self, _p: &ClientboundSelectAdvancementsTab) {}
+ pub fn set_action_bar_text(&mut self, _p: &ClientboundSetActionBarText) {}
+ pub fn set_border_center(&mut self, _p: &ClientboundSetBorderCenter) {}
+ pub fn set_border_lerp_size(&mut self, _p: &ClientboundSetBorderLerpSize) {}
+ pub fn set_border_size(&mut self, _p: &ClientboundSetBorderSize) {}
+ pub fn set_border_warning_delay(&mut self, _p: &ClientboundSetBorderWarningDelay) {}
+ pub fn set_border_warning_distance(&mut self, _p: &ClientboundSetBorderWarningDistance) {}
+ pub fn set_camera(&mut self, _p: &ClientboundSetCamera) {}
+ pub fn set_display_objective(&mut self, _p: &ClientboundSetDisplayObjective) {}
+ pub fn set_objective(&mut self, _p: &ClientboundSetObjective) {}
+ pub fn set_passengers(&mut self, _p: &ClientboundSetPassengers) {}
+ pub fn set_player_team(&mut self, _p: &ClientboundSetPlayerTeam) {}
+ pub fn set_score(&mut self, _p: &ClientboundSetScore) {}
+ pub fn set_simulation_distance(&mut self, _p: &ClientboundSetSimulationDistance) {}
+ pub fn set_subtitle_text(&mut self, _p: &ClientboundSetSubtitleText) {}
+ pub fn set_title_text(&mut self, _p: &ClientboundSetTitleText) {}
+ pub fn set_titles_animation(&mut self, _p: &ClientboundSetTitlesAnimation) {}
+ pub fn clear_titles(&mut self, _p: &ClientboundClearTitles) {}
+ pub fn sound_entity(&mut self, _p: &ClientboundSoundEntity) {}
+ pub fn stop_sound(&mut self, _p: &ClientboundStopSound) {}
+ pub fn tab_list(&mut self, _p: &ClientboundTabList) {}
+ pub fn tag_query(&mut self, _p: &ClientboundTagQuery) {}
+ pub fn take_item_entity(&mut self, _p: &ClientboundTakeItemEntity) {}
+ pub fn bundle_delimiter(&mut self, _p: &ClientboundBundleDelimiter) {}
+ pub fn damage_event(&mut self, _p: &ClientboundDamageEvent) {}
+ pub fn hurt_animation(&mut self, _p: &ClientboundHurtAnimation) {}
+ pub fn ticking_state(&mut self, _p: &ClientboundTickingState) {}
+ pub fn ticking_step(&mut self, _p: &ClientboundTickingStep) {}
+ pub fn reset_score(&mut self, _p: &ClientboundResetScore) {}
+ pub fn cookie_request(&mut self, _p: &ClientboundCookieRequest) {}
+ pub fn debug_sample(&mut self, _p: &ClientboundDebugSample) {}
+ pub fn pong_response(&mut self, _p: &ClientboundPongResponse) {}
+ pub fn store_cookie(&mut self, _p: &ClientboundStoreCookie) {}
+ pub fn transfer(&mut self, _p: &ClientboundTransfer) {}
+ pub fn move_minecart_along_track(&mut self, _p: &ClientboundMoveMinecartAlongTrack) {}
+ pub fn set_held_slot(&mut self, _p: &ClientboundSetHeldSlot) {}
+ pub fn set_player_inventory(&mut self, _p: &ClientboundSetPlayerInventory) {}
+ pub fn projectile_power(&mut self, _p: &ClientboundProjectilePower) {}
+ pub fn custom_report_details(&mut self, _p: &ClientboundCustomReportDetails) {}
+ pub fn server_links(&mut self, _p: &ClientboundServerLinks) {}
+ pub fn player_rotation(&mut self, _p: &ClientboundPlayerRotation) {}
+ pub fn recipe_book_add(&mut self, _p: &ClientboundRecipeBookAdd) {}
+ pub fn recipe_book_remove(&mut self, _p: &ClientboundRecipeBookRemove) {}
+ pub fn recipe_book_settings(&mut self, _p: &ClientboundRecipeBookSettings) {}
+}
diff --git a/azalea-client/src/plugins/packet/login.rs b/azalea-client/src/plugins/packet/login.rs
new file mode 100644
index 00000000..1bb07266
--- /dev/null
+++ b/azalea-client/src/plugins/packet/login.rs
@@ -0,0 +1,114 @@
+// login packets aren't actually handled here because compression/encryption
+// would make packet handling a lot messier
+
+use std::{collections::HashSet, sync::Arc};
+
+use azalea_protocol::packets::{
+ Packet,
+ login::{
+ ClientboundLoginPacket, ServerboundLoginPacket,
+ s_custom_query_answer::ServerboundCustomQueryAnswer,
+ },
+};
+use bevy_ecs::{prelude::*, system::SystemState};
+use derive_more::{Deref, DerefMut};
+use tokio::sync::mpsc;
+use tracing::error;
+
+// this struct is defined here anyways though so it's consistent with the other
+// ones
+
+/// An event that's sent when we receive a login packet from the server. Note
+/// that if you want to handle this in a system, you must add
+/// `.before(azalea::packet::login::process_packet_events)` to it
+/// because that system clears the events.
+#[derive(Event, Debug, Clone)]
+pub struct LoginPacketEvent {
+ /// The client entity that received the packet.
+ pub entity: Entity,
+ /// The packet that was actually received.
+ pub packet: Arc<ClientboundLoginPacket>,
+}
+
+/// Event for sending a login packet to the server.
+#[derive(Event)]
+pub struct SendLoginPacketEvent {
+ pub entity: Entity,
+ pub packet: ServerboundLoginPacket,
+}
+impl SendLoginPacketEvent {
+ pub fn new(entity: Entity, packet: impl Packet<ServerboundLoginPacket>) -> Self {
+ let packet = packet.into_variant();
+ Self { entity, packet }
+ }
+}
+
+#[derive(Component)]
+pub struct LoginSendPacketQueue {
+ pub tx: mpsc::UnboundedSender<ServerboundLoginPacket>,
+}
+
+/// A marker component for local players that are currently in the
+/// `login` state.
+#[derive(Component, Clone, Debug)]
+pub struct InLoginState;
+
+pub fn handle_send_packet_event(
+ mut send_packet_events: EventReader<SendLoginPacketEvent>,
+ mut query: Query<&mut LoginSendPacketQueue>,
+) {
+ for event in send_packet_events.read() {
+ if let Ok(queue) = query.get_mut(event.entity) {
+ let _ = queue.tx.send(event.packet.clone());
+ } else {
+ error!("Sent SendPacketEvent for entity that doesn't have a LoginSendPacketQueue");
+ }
+ }
+}
+
+/// Plugins can add to this set if they want to handle a custom query packet
+/// themselves. This component removed after the login state ends.
+#[derive(Component, Default, Debug, Deref, DerefMut)]
+pub struct IgnoreQueryIds(HashSet<u32>);
+
+pub fn process_packet_events(ecs: &mut World) {
+ let mut events_owned = Vec::new();
+ let mut system_state: SystemState<ResMut<Events<LoginPacketEvent>>> = SystemState::new(ecs);
+ let mut events = system_state.get_mut(ecs);
+ for LoginPacketEvent {
+ entity: player_entity,
+ packet,
+ } in events.drain()
+ {
+ // we do this so `ecs` isn't borrowed for the whole loop
+ events_owned.push((player_entity, packet));
+ }
+ for (player_entity, packet) in events_owned {
+ #[allow(clippy::single_match)]
+ match packet.as_ref() {
+ ClientboundLoginPacket::CustomQuery(p) => {
+ let mut system_state: SystemState<(
+ EventWriter<SendLoginPacketEvent>,
+ Query<&IgnoreQueryIds>,
+ )> = SystemState::new(ecs);
+ let (mut send_packet_events, query) = system_state.get_mut(ecs);
+
+ let ignore_query_ids = query.get(player_entity).ok().map(|x| x.0.clone());
+ if let Some(ignore_query_ids) = ignore_query_ids {
+ if ignore_query_ids.contains(&p.transaction_id) {
+ continue;
+ }
+ }
+
+ send_packet_events.send(SendLoginPacketEvent::new(
+ player_entity,
+ ServerboundCustomQueryAnswer {
+ transaction_id: p.transaction_id,
+ data: None,
+ },
+ ));
+ }
+ _ => {}
+ }
+ }
+}
diff --git a/azalea-client/src/plugins/packet/mod.rs b/azalea-client/src/plugins/packet/mod.rs
new file mode 100644
index 00000000..cbd8a175
--- /dev/null
+++ b/azalea-client/src/plugins/packet/mod.rs
@@ -0,0 +1,109 @@
+use azalea_entity::{EntityUpdateSet, metadata::Health};
+use bevy_app::{App, First, Plugin, PreUpdate, Update};
+use bevy_ecs::{
+ prelude::*,
+ system::{SystemParam, SystemState},
+};
+
+use self::{
+ game::{
+ AddPlayerEvent, DeathEvent, InstanceLoadedEvent, KeepAliveEvent, RemovePlayerEvent,
+ ResourcePackEvent, UpdatePlayerEvent,
+ },
+ login::{LoginPacketEvent, SendLoginPacketEvent},
+};
+use crate::{chat::ChatReceivedEvent, events::death_listener};
+
+pub mod config;
+pub mod game;
+pub mod login;
+
+pub struct PacketPlugin;
+
+pub fn death_event_on_0_health(
+ query: Query<(Entity, &Health), Changed<Health>>,
+ mut death_events: EventWriter<DeathEvent>,
+) {
+ for (entity, health) in query.iter() {
+ if **health == 0. {
+ death_events.send(DeathEvent {
+ entity,
+ packet: None,
+ });
+ }
+ }
+}
+
+impl Plugin for PacketPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_systems(
+ First,
+ (game::send_receivepacketevent, config::send_packet_events),
+ )
+ .add_systems(
+ PreUpdate,
+ (
+ game::process_packet_events
+ // we want to index and deindex right after
+ .before(EntityUpdateSet::Deindex),
+ config::process_packet_events,
+ login::handle_send_packet_event,
+ login::process_packet_events,
+ ),
+ )
+ .add_systems(
+ Update,
+ (
+ (
+ config::handle_send_packet_event,
+ game::handle_outgoing_packets,
+ )
+ .chain(),
+ death_event_on_0_health.before(death_listener),
+ ),
+ )
+ // we do this instead of add_event so we can handle the events ourselves
+ .init_resource::<Events<game::ReceivePacketEvent>>()
+ .init_resource::<Events<config::ReceiveConfigPacketEvent>>()
+ .add_event::<game::SendPacketEvent>()
+ .add_event::<config::SendConfigPacketEvent>()
+ .add_event::<AddPlayerEvent>()
+ .add_event::<RemovePlayerEvent>()
+ .add_event::<UpdatePlayerEvent>()
+ .add_event::<ChatReceivedEvent>()
+ .add_event::<DeathEvent>()
+ .add_event::<KeepAliveEvent>()
+ .add_event::<ResourcePackEvent>()
+ .add_event::<InstanceLoadedEvent>()
+ .add_event::<LoginPacketEvent>()
+ .add_event::<SendLoginPacketEvent>();
+ }
+}
+
+#[macro_export]
+macro_rules! declare_packet_handlers {
+ (
+ $packetenum:ident,
+ $packetvar:expr,
+ $handler:ident,
+ [$($packet:path),+ $(,)?]
+ ) => {
+ paste::paste! {
+ match $packetvar {
+ $(
+ $packetenum::[< $packet:camel >](p) => $handler.$packet(p),
+ )+
+ }
+ }
+ };
+}
+
+pub(crate) fn as_system<T>(ecs: &mut World, f: impl FnOnce(T::Item<'_, '_>))
+where
+ T: SystemParam + 'static,
+{
+ let mut system_state = SystemState::<T>::new(ecs);
+ let values = system_state.get_mut(ecs);
+ f(values);
+ system_state.apply(ecs);
+}