aboutsummaryrefslogtreecommitdiff
path: root/azalea-client/src/packet_handling
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2023-09-21 11:16:29 -0500
committerGitHub <noreply@github.com>2023-09-21 11:16:29 -0500
commit7b3e2e4bf793466a351510c7fbbd08234e93bb0e (patch)
tree7177a919de9982d9e3c7f36a76d2025696f465b6 /azalea-client/src/packet_handling
parent83cce236145cdab1872a472a70943b669a880965 (diff)
downloadazalea-drasl-7b3e2e4bf793466a351510c7fbbd08234e93bb0e.tar.xz
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 <git@matdoes.dev>
Diffstat (limited to 'azalea-client/src/packet_handling')
-rw-r--r--azalea-client/src/packet_handling/configuration.rs204
-rw-r--r--azalea-client/src/packet_handling/game.rs1295
-rw-r--r--azalea-client/src/packet_handling/mod.rs59
3 files changed, 1558 insertions, 0 deletions
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<InConfigurationState>>,
+ mut packet_events: ResMut<Events<PacketEvent>>,
+) {
+ // 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::<ClientboundConfigurationPacket>(
+ &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<EventReader<PacketEvent>> = 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<Query<&mut ReceivedRegistries>> =
+ 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<EventWriter<DisconnectEvent>> =
+ 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<Query<&mut RawConnection>> =
+ 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::<InConfigurationState>()
+ .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<KeepAliveEvent>,
+ )> = 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<Query<&RawConnection>> = 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<Query<&RawConnection>> = 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<PacketEvent>) {
+/// 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<ClientboundPlayerCombatKillPacket>,
+}
+
+/// 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<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>>,
+}
+
+pub fn send_packet_events(
+ query: Query<(Entity, &RawConnection), With<LocalEntity>>,
+ mut packet_events: ResMut<Events<PacketEvent>>,
+) {
+ // 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);
+ 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<EventReader<PacketEvent>> = 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<InstanceLoadedEvent>,
+ ResMut<InstanceContainer>,
+ EventWriter<SendPacketEvent>,
+ )> = 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<chunk_batching::ChunkBatchStartEvent>,
+ > = 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<chunk_batching::ChunkBatchFinishedEvent>,
+ > = 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<Query<&mut PlayerAbilities>> =
+ 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<EventWriter<DisconnectEvent>> =
+ 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<SendPacketEvent>,
+ )> = 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<AddPlayerEvent>,
+ EventWriter<UpdatePlayerEvent>,
+ ResMut<TabList>,
+ )> = 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<RemovePlayerEvent>,
+ ResMut<TabList>,
+ )> = 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<Query<&mut InstanceHolder>> =
+ 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<Query<&mut InstanceHolder>> =
+ 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<InstanceContainer>,
+ ResMut<EntityUuidIndex>,
+ )> = 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::<Commands>::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<Query<(&mut Health, &mut Hunger)>> =
+ 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::<Position>().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::<Position>().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::<Position>().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<KeepAliveEvent>,
+ EventWriter<SendPacketEvent>,
+ )> = 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<EventWriter<ChatReceivedEvent>> =
+ 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<EventWriter<ChatReceivedEvent>> =
+ 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<Query<&mut InstanceHolder>> =
+ 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<Query<&mut InstanceHolder>> =
+ 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<Query<&mut LocalGameMode>> =
+ 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<SetContainerContentEvent>,
+ )> = 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<Query<&mut
+ // InventoryComponent>> =
+ // 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<Query<&mut InventoryComponent>> =
+ 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<EventWriter<ClientSideCloseContainerEvent>> =
+ 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<EventWriter<MenuOpenedEvent>> =
+ 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<EventWriter<SendPacketEvent>> =
+ 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<DeathEvent>,
+ )> = 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<EventWriter<ResourcePackEvent>> =
+ 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<InstanceLoadedEvent>,
+ ResMut<InstanceContainer>,
+ )> = 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::<Dead>();
+
+ 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<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 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::<Events<game::PacketEvent>>()
+ .init_resource::<Events<configuration::PacketEvent>>()
+ .add_event::<AddPlayerEvent>()
+ .add_event::<RemovePlayerEvent>()
+ .add_event::<UpdatePlayerEvent>()
+ .add_event::<ChatReceivedEvent>()
+ .add_event::<DeathEvent>()
+ .add_event::<KeepAliveEvent>()
+ .add_event::<ResourcePackEvent>()
+ .add_event::<InstanceLoadedEvent>();
+ }
+}