diff options
| author | mat <27899617+mat-1@users.noreply.github.com> | 2023-09-21 11:16:29 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-09-21 11:16:29 -0500 |
| commit | 7b3e2e4bf793466a351510c7fbbd08234e93bb0e (patch) | |
| tree | 7177a919de9982d9e3c7f36a76d2025696f465b6 /azalea-client/src/packet_handling | |
| parent | 83cce236145cdab1872a472a70943b669a880965 (diff) | |
| download | azalea-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.rs | 204 | ||||
| -rw-r--r-- | azalea-client/src/packet_handling/game.rs | 1295 | ||||
| -rw-r--r-- | azalea-client/src/packet_handling/mod.rs | 59 |
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>(); + } +} |
