aboutsummaryrefslogtreecommitdiff
path: root/azalea-client/src
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2025-02-22 21:45:26 -0600
committerGitHub <noreply@github.com>2025-02-22 21:45:26 -0600
commite21e1b97bf9337e9f4747cd1b545b1b3a03e2ce7 (patch)
treeadd6f8bfce40d0c07845d8aa4c9945a0b918444c /azalea-client/src
parentf8130c3c92946d2293634ba4e252d6bc93026c3c (diff)
downloadazalea-drasl-e21e1b97bf9337e9f4747cd1b545b1b3a03e2ce7.tar.xz
Refactor azalea-client (#205)
* start organizing packet_handling more by moving packet handlers into their own functions * finish writing all the handler functions for packets * use macro for generating match statement for packet handler functions * fix set_entity_data * update config state to also use handler functions * organize az-client file structure by moving things into plugins directory * fix merge issues
Diffstat (limited to 'azalea-client/src')
-rw-r--r--azalea-client/src/client.rs37
-rw-r--r--azalea-client/src/lib.rs16
-rw-r--r--azalea-client/src/local_player.rs13
-rw-r--r--azalea-client/src/packet_handling/configuration.rs271
-rw-r--r--azalea-client/src/packet_handling/game.rs1585
-rwxr-xr-xazalea-client/src/player.rs2
-rw-r--r--azalea-client/src/plugins/attack.rs (renamed from azalea-client/src/attack.rs)3
-rw-r--r--azalea-client/src/plugins/brand.rs (renamed from azalea-client/src/configuration.rs)19
-rw-r--r--azalea-client/src/plugins/chat/handler.rs61
-rw-r--r--[-rwxr-xr-x]azalea-client/src/plugins/chat/mod.rs (renamed from azalea-client/src/chat.rs)111
-rw-r--r--azalea-client/src/plugins/chunks.rs (renamed from azalea-client/src/chunks.rs)14
-rw-r--r--azalea-client/src/plugins/disconnect.rs (renamed from azalea-client/src/disconnect.rs)0
-rw-r--r--azalea-client/src/plugins/events.rs (renamed from azalea-client/src/events.rs)54
-rw-r--r--azalea-client/src/plugins/interact.rs (renamed from azalea-client/src/interact.rs)5
-rw-r--r--azalea-client/src/plugins/inventory.rs (renamed from azalea-client/src/inventory.rs)8
-rw-r--r--azalea-client/src/plugins/mining.rs (renamed from azalea-client/src/mining.rs)7
-rw-r--r--azalea-client/src/plugins/mod.rs14
-rw-r--r--azalea-client/src/plugins/movement.rs (renamed from azalea-client/src/movement.rs)6
-rw-r--r--azalea-client/src/plugins/packet/config/events.rs90
-rw-r--r--azalea-client/src/plugins/packet/config/mod.rs223
-rw-r--r--azalea-client/src/plugins/packet/game/events.rs178
-rw-r--r--azalea-client/src/plugins/packet/game/mod.rs1583
-rw-r--r--azalea-client/src/plugins/packet/login.rs (renamed from azalea-client/src/packet_handling/login.rs)2
-rw-r--r--azalea-client/src/plugins/packet/mod.rs (renamed from azalea-client/src/packet_handling/mod.rs)53
-rw-r--r--azalea-client/src/plugins/respawn.rs (renamed from azalea-client/src/respawn.rs)5
-rw-r--r--azalea-client/src/plugins/task_pool.rs (renamed from azalea-client/src/task_pool.rs)0
-rw-r--r--azalea-client/src/plugins/tick_end.rs (renamed from azalea-client/src/send_client_end.rs)2
27 files changed, 2309 insertions, 2053 deletions
diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs
index 2f7460f5..7a1c3ae0 100644
--- a/azalea-client/src/client.rs
+++ b/azalea-client/src/client.rs
@@ -63,28 +63,27 @@ use uuid::Uuid;
use crate::{
Account, PlayerInfo,
attack::{self, AttackPlugin},
+ brand::BrandPlugin,
chat::ChatPlugin,
- chunks::{ChunkBatchInfo, ChunkPlugin},
- configuration::ConfigurationPlugin,
+ chunks::{ChunkBatchInfo, ChunksPlugin},
disconnect::{DisconnectEvent, DisconnectPlugin},
- events::{Event, EventPlugin, LocalPlayerEvents},
+ events::{Event, EventsPlugin, LocalPlayerEvents},
interact::{CurrentSequenceNumber, InteractPlugin},
inventory::{Inventory, InventoryPlugin},
local_player::{
GameProfileComponent, Hunger, InstanceHolder, PermissionLevel, PlayerAbilities, TabList,
- death_event,
},
- mining::{self, MinePlugin},
- movement::{LastSentLookDirection, PhysicsState, PlayerMovePlugin},
- packet_handling::{
- PacketHandlerPlugin,
+ mining::{self, MiningPlugin},
+ movement::{LastSentLookDirection, MovementPlugin, PhysicsState},
+ packet::{
+ PacketPlugin,
login::{self, InLoginState, LoginSendPacketQueue},
},
player::retroactively_add_game_profile_component,
raw_connection::RawConnection,
respawn::RespawnPlugin,
- send_client_end::TickEndPlugin,
task_pool::TaskPoolPlugin,
+ tick_end::TickEndPlugin,
};
/// `Client` has the things that a user interacting with the library will want.
@@ -370,7 +369,7 @@ impl Client {
let (ecs_packets_tx, mut ecs_packets_rx) = mpsc::unbounded_channel();
ecs_lock.lock().entity_mut(entity).insert((
LoginSendPacketQueue { tx: ecs_packets_tx },
- login::IgnoreQueryIds::default(),
+ crate::packet::login::IgnoreQueryIds::default(),
InLoginState,
));
@@ -468,7 +467,7 @@ impl Client {
ClientboundLoginPacket::CustomQuery(p) => {
debug!("Got custom query {:?}", p);
// replying to custom query is done in
- // packet_handling::login::process_packet_events
+ // packet::login::process_packet_events
}
ClientboundLoginPacket::CookieRequest(p) => {
debug!("Got cookie request {:?}", p);
@@ -794,7 +793,7 @@ pub struct LocalPlayerBundle {
/// A bundle for the components that are present on a local player that is
/// currently in the `game` protocol state. If you want to filter for this, just
/// use [`LocalEntity`].
-#[derive(Bundle)]
+#[derive(Bundle, Default)]
pub struct JoinedClientBundle {
// note that InstanceHolder isn't here because it's set slightly before we fully join the world
pub physics_state: PhysicsState,
@@ -826,8 +825,6 @@ impl Plugin for AzaleaPlugin {
app.add_systems(
Update,
(
- // fire the Death event when the player dies.
- death_event,
// add GameProfileComponent when we get an AddPlayerEvent
retroactively_add_game_profile_component.after(EntityUpdateSet::Index),
),
@@ -972,23 +969,23 @@ impl PluginGroup for DefaultPlugins {
let mut group = PluginGroupBuilder::start::<Self>()
.add(AmbiguityLoggerPlugin)
.add(TimePlugin)
- .add(PacketHandlerPlugin)
+ .add(PacketPlugin)
.add(AzaleaPlugin)
.add(EntityPlugin)
.add(PhysicsPlugin)
- .add(EventPlugin)
+ .add(EventsPlugin)
.add(TaskPoolPlugin::default())
.add(InventoryPlugin)
.add(ChatPlugin)
.add(DisconnectPlugin)
- .add(PlayerMovePlugin)
+ .add(MovementPlugin)
.add(InteractPlugin)
.add(RespawnPlugin)
- .add(MinePlugin)
+ .add(MiningPlugin)
.add(AttackPlugin)
- .add(ChunkPlugin)
+ .add(ChunksPlugin)
.add(TickEndPlugin)
- .add(ConfigurationPlugin)
+ .add(BrandPlugin)
.add(TickBroadcastPlugin);
#[cfg(feature = "log")]
{
diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs
index abe7c692..d2302b78 100644
--- a/azalea-client/src/lib.rs
+++ b/azalea-client/src/lib.rs
@@ -8,26 +8,13 @@
#![feature(error_generic_member_access)]
mod account;
-pub mod attack;
-pub mod chat;
-pub mod chunks;
mod client;
-pub mod configuration;
-pub mod disconnect;
mod entity_query;
-pub mod events;
-pub mod interact;
-pub mod inventory;
mod local_player;
-pub mod mining;
-pub mod movement;
-pub mod packet_handling;
pub mod ping;
mod player;
+mod plugins;
pub mod raw_connection;
-pub mod respawn;
-pub mod send_client_end;
-pub mod task_pool;
#[doc(hidden)]
pub mod test_simulation;
@@ -44,3 +31,4 @@ pub use movement::{
PhysicsState, SprintDirection, StartSprintEvent, StartWalkEvent, WalkDirection,
};
pub use player::PlayerInfo;
+pub use plugins::*;
diff --git a/azalea-client/src/local_player.rs b/azalea-client/src/local_player.rs
index 7e323f4c..455cc470 100644
--- a/azalea-client/src/local_player.rs
+++ b/azalea-client/src/local_player.rs
@@ -2,7 +2,6 @@ use std::{collections::HashMap, io, sync::Arc};
use azalea_auth::game_profile::GameProfile;
use azalea_core::game_type::GameMode;
-use azalea_entity::Dead;
use azalea_protocol::packets::game::c_player_abilities::ClientboundPlayerAbilities;
use azalea_world::{Instance, PartialInstance};
use bevy_ecs::{component::Component, prelude::*};
@@ -13,10 +12,7 @@ use tokio::sync::mpsc;
use tracing::error;
use uuid::Uuid;
-use crate::{
- ClientInformation, PlayerInfo,
- events::{Event as AzaleaEvent, LocalPlayerEvents},
-};
+use crate::{ClientInformation, PlayerInfo, events::Event as AzaleaEvent};
/// A component that keeps strong references to our [`PartialInstance`] and
/// [`Instance`] for local players.
@@ -150,13 +146,6 @@ impl InstanceHolder {
}
}
-/// Send the "Death" event for [`LocalEntity`]s that died with no reason.
-pub fn death_event(query: Query<&LocalPlayerEvents, Added<Dead>>) {
- for local_player_events in &query {
- local_player_events.send(AzaleaEvent::Death(None)).unwrap();
- }
-}
-
#[derive(Error, Debug)]
pub enum HandlePacketError {
#[error("{0}")]
diff --git a/azalea-client/src/packet_handling/configuration.rs b/azalea-client/src/packet_handling/configuration.rs
deleted file mode 100644
index bfa6914b..00000000
--- a/azalea-client/src/packet_handling/configuration.rs
+++ /dev/null
@@ -1,271 +0,0 @@
-use std::io::Cursor;
-
-use azalea_entity::indexing::EntityIdIndex;
-use azalea_protocol::packets::config::s_finish_configuration::ServerboundFinishConfiguration;
-use azalea_protocol::packets::config::s_keep_alive::ServerboundKeepAlive;
-use azalea_protocol::packets::config::s_select_known_packs::ServerboundSelectKnownPacks;
-use azalea_protocol::packets::config::{
- self, ClientboundConfigPacket, ServerboundConfigPacket, ServerboundCookieResponse,
- ServerboundResourcePack,
-};
-use azalea_protocol::packets::{ConnectionProtocol, Packet};
-use azalea_protocol::read::deserialize_packet;
-use bevy_ecs::prelude::*;
-use bevy_ecs::system::SystemState;
-use tracing::{debug, error, warn};
-
-use crate::InstanceHolder;
-use crate::client::InConfigState;
-use crate::disconnect::DisconnectEvent;
-use crate::local_player::Hunger;
-use crate::packet_handling::game::KeepAliveEvent;
-use crate::raw_connection::RawConnection;
-
-#[derive(Event, Debug, Clone)]
-pub struct ConfigurationEvent {
- /// The client entity that received the packet.
- pub entity: Entity,
- /// The packet that was actually received.
- pub packet: ClientboundConfigPacket,
-}
-
-pub fn send_packet_events(
- query: Query<(Entity, &RawConnection), With<InConfigState>>,
- mut packet_events: ResMut<Events<ConfigurationEvent>>,
-) {
- // we manually clear and send the events at the beginning of each update
- // since otherwise it'd cause issues with events in process_packet_events
- // running twice
- packet_events.clear();
- for (player_entity, raw_conn) in &query {
- let packets_lock = raw_conn.incoming_packet_queue();
- let mut packets = packets_lock.lock();
- if !packets.is_empty() {
- for raw_packet in packets.iter() {
- let packet = match deserialize_packet::<ClientboundConfigPacket>(&mut Cursor::new(
- raw_packet,
- )) {
- Ok(packet) => packet,
- Err(err) => {
- error!("failed to read packet: {err:?}");
- debug!("packet bytes: {raw_packet:?}");
- continue;
- }
- };
- packet_events.send(ConfigurationEvent {
- entity: player_entity,
- packet,
- });
- }
- // clear the packets right after we read them
- packets.clear();
- }
- }
-}
-
-pub fn process_packet_events(ecs: &mut World) {
- let mut events_owned = Vec::new();
- let mut system_state: SystemState<EventReader<ConfigurationEvent>> = SystemState::new(ecs);
- let mut events = system_state.get_mut(ecs);
- for ConfigurationEvent {
- entity: player_entity,
- packet,
- } in events.read()
- {
- // we do this so `ecs` isn't borrowed for the whole loop
- events_owned.push((*player_entity, packet.clone()));
- }
- for (player_entity, packet) in events_owned {
- match packet {
- ClientboundConfigPacket::RegistryData(p) => {
- let mut system_state: SystemState<Query<&mut InstanceHolder>> =
- SystemState::new(ecs);
- let mut query = system_state.get_mut(ecs);
- let instance_holder = query.get_mut(player_entity).unwrap();
- let mut instance = instance_holder.instance.write();
-
- // add the new registry data
- instance.registries.append(p.registry_id, p.entries);
- }
-
- ClientboundConfigPacket::CustomPayload(p) => {
- debug!("Got custom payload packet {p:?}");
- }
- ClientboundConfigPacket::Disconnect(p) => {
- warn!("Got disconnect packet {p:?}");
- let mut system_state: SystemState<EventWriter<DisconnectEvent>> =
- SystemState::new(ecs);
- let mut disconnect_events = system_state.get_mut(ecs);
- disconnect_events.send(DisconnectEvent {
- entity: player_entity,
- reason: Some(p.reason.clone()),
- });
- }
- ClientboundConfigPacket::FinishConfiguration(p) => {
- debug!("got FinishConfiguration packet: {p:?}");
-
- let mut system_state: SystemState<Query<&mut RawConnection>> =
- SystemState::new(ecs);
- let mut query = system_state.get_mut(ecs);
- let mut raw_conn = query.get_mut(player_entity).unwrap();
-
- raw_conn
- .write_packet(ServerboundFinishConfiguration)
- .expect(
- "we should be in the right state and encoding this packet shouldn't fail",
- );
- raw_conn.set_state(ConnectionProtocol::Game);
-
- // these components are added now that we're going to be in the Game state
- ecs.entity_mut(player_entity)
- .remove::<InConfigState>()
- .insert(crate::JoinedClientBundle {
- physics_state: crate::PhysicsState::default(),
- inventory: crate::inventory::Inventory::default(),
- tab_list: crate::local_player::TabList::default(),
- current_sequence_number: crate::interact::CurrentSequenceNumber::default(),
- last_sent_direction: crate::movement::LastSentLookDirection::default(),
- abilities: crate::local_player::PlayerAbilities::default(),
- permission_level: crate::local_player::PermissionLevel::default(),
- hunger: Hunger::default(),
- chunk_batch_info: crate::chunks::ChunkBatchInfo::default(),
-
- entity_id_index: EntityIdIndex::default(),
-
- mining: crate::mining::MineBundle::default(),
- attack: crate::attack::AttackBundle::default(),
-
- _local_entity: azalea_entity::LocalEntity,
- });
- }
- ClientboundConfigPacket::KeepAlive(p) => {
- debug!("Got keep alive packet (in configuration) {p:?} for {player_entity:?}");
-
- let mut system_state: SystemState<(
- Query<&RawConnection>,
- EventWriter<KeepAliveEvent>,
- )> = SystemState::new(ecs);
- let (query, mut keepalive_events) = system_state.get_mut(ecs);
- let raw_conn = query.get(player_entity).unwrap();
-
- keepalive_events.send(KeepAliveEvent {
- entity: player_entity,
- id: p.id,
- });
- raw_conn
- .write_packet(ServerboundKeepAlive { id: p.id })
- .unwrap();
- }
- ClientboundConfigPacket::Ping(p) => {
- debug!("Got ping packet {p:?}");
-
- let mut system_state: SystemState<Query<&RawConnection>> = SystemState::new(ecs);
- let mut query = system_state.get_mut(ecs);
- let raw_conn = query.get_mut(player_entity).unwrap();
-
- raw_conn
- .write_packet(config::s_pong::ServerboundPong { id: p.id })
- .unwrap();
- }
- ClientboundConfigPacket::ResourcePackPush(p) => {
- debug!("Got resource pack packet {p:?}");
-
- let mut system_state: SystemState<Query<&RawConnection>> = SystemState::new(ecs);
- let mut query = system_state.get_mut(ecs);
- let raw_conn = query.get_mut(player_entity).unwrap();
-
- // always accept resource pack
- raw_conn
- .write_packet(ServerboundResourcePack {
- id: p.id,
- action: config::s_resource_pack::Action::Accepted,
- })
- .unwrap();
- }
- ClientboundConfigPacket::ResourcePackPop(_) => {
- // we can ignore this
- }
- ClientboundConfigPacket::UpdateEnabledFeatures(p) => {
- debug!("Got update enabled features packet {p:?}");
- }
- ClientboundConfigPacket::UpdateTags(_p) => {
- debug!("Got update tags packet");
- }
- ClientboundConfigPacket::CookieRequest(p) => {
- debug!("Got cookie request packet {p:?}");
-
- let mut system_state: SystemState<Query<&RawConnection>> = SystemState::new(ecs);
- let mut query = system_state.get_mut(ecs);
- let raw_conn = query.get_mut(player_entity).unwrap();
-
- raw_conn
- .write_packet(ServerboundCookieResponse {
- key: p.key,
- // cookies aren't implemented
- payload: None,
- })
- .unwrap();
- }
- ClientboundConfigPacket::ResetChat(p) => {
- debug!("Got reset chat packet {p:?}");
- }
- ClientboundConfigPacket::StoreCookie(p) => {
- debug!("Got store cookie packet {p:?}");
- }
- ClientboundConfigPacket::Transfer(p) => {
- debug!("Got transfer packet {p:?}");
- }
- ClientboundConfigPacket::SelectKnownPacks(p) => {
- debug!("Got select known packs packet {p:?}");
-
- let mut system_state: SystemState<Query<&RawConnection>> = SystemState::new(ecs);
- let mut query = system_state.get_mut(ecs);
- let raw_conn = query.get_mut(player_entity).unwrap();
-
- // resource pack management isn't implemented
- raw_conn
- .write_packet(ServerboundSelectKnownPacks {
- known_packs: vec![],
- })
- .unwrap();
- }
- ClientboundConfigPacket::ServerLinks(_) => {}
- ClientboundConfigPacket::CustomReportDetails(_) => {}
- }
- }
-}
-
-/// An event for sending a packet to the server while we're in the
-/// `configuration` state.
-#[derive(Event)]
-pub struct SendConfigurationEvent {
- pub sent_by: Entity,
- pub packet: ServerboundConfigPacket,
-}
-impl SendConfigurationEvent {
- pub fn new(sent_by: Entity, packet: impl Packet<ServerboundConfigPacket>) -> Self {
- let packet = packet.into_variant();
- Self { sent_by, packet }
- }
-}
-
-pub fn handle_send_packet_event(
- mut send_packet_events: EventReader<SendConfigurationEvent>,
- mut query: Query<(&mut RawConnection, Option<&InConfigState>)>,
-) {
- for event in send_packet_events.read() {
- if let Ok((raw_conn, in_configuration_state)) = query.get_mut(event.sent_by) {
- if in_configuration_state.is_none() {
- error!(
- "Tried to send a configuration packet {:?} while not in configuration state",
- event.packet
- );
- continue;
- }
- debug!("Sending packet: {:?}", event.packet);
- if let Err(e) = raw_conn.write_packet(event.packet.clone()) {
- error!("Failed to send packet: {e}");
- }
- }
- }
-}
diff --git a/azalea-client/src/packet_handling/game.rs b/azalea-client/src/packet_handling/game.rs
deleted file mode 100644
index 6f2868e9..00000000
--- a/azalea-client/src/packet_handling/game.rs
+++ /dev/null
@@ -1,1585 +0,0 @@
-use std::{
- collections::HashSet,
- io::Cursor,
- ops::Add,
- sync::{Arc, Weak},
-};
-
-use azalea_chat::FormattedText;
-use azalea_core::{
- game_type::GameMode,
- math,
- position::{ChunkPos, Vec3},
- resource_location::ResourceLocation,
-};
-use azalea_entity::{
- Dead, EntityBundle, EntityKind, LastSentPosition, LoadedBy, LocalEntity, LookDirection,
- Physics, Position, RelativeEntityUpdate,
- indexing::{EntityIdIndex, EntityUuidIndex},
- metadata::{Health, apply_metadata},
-};
-use azalea_protocol::{
- packets::{
- Packet,
- game::{
- ClientboundGamePacket, ServerboundGamePacket,
- c_player_combat_kill::ClientboundPlayerCombatKill,
- s_accept_teleportation::ServerboundAcceptTeleportation,
- s_configuration_acknowledged::ServerboundConfigurationAcknowledged,
- s_keep_alive::ServerboundKeepAlive, s_move_player_pos_rot::ServerboundMovePlayerPosRot,
- s_pong::ServerboundPong,
- },
- },
- read::deserialize_packet,
-};
-use azalea_world::{Instance, InstanceContainer, InstanceName, MinecraftEntityId, PartialInstance};
-use bevy_ecs::{prelude::*, system::SystemState};
-use parking_lot::RwLock;
-use tracing::{debug, error, trace, warn};
-use uuid::Uuid;
-
-use crate::{
- ClientInformation, PlayerInfo,
- chat::{ChatPacket, ChatReceivedEvent},
- chunks,
- disconnect::DisconnectEvent,
- inventory::{
- ClientSideCloseContainerEvent, Inventory, MenuOpenedEvent, SetContainerContentEvent,
- },
- local_player::{
- GameProfileComponent, Hunger, InstanceHolder, LocalGameMode, PlayerAbilities, TabList,
- },
- movement::{KnockbackEvent, KnockbackType},
- raw_connection::RawConnection,
-};
-
-/// An event that's sent when we receive a packet.
-/// ```
-/// # use azalea_client::packet_handling::game::PacketEvent;
-/// # use azalea_protocol::packets::game::ClientboundGamePacket;
-/// # use bevy_ecs::event::EventReader;
-///
-/// fn handle_packets(mut events: EventReader<PacketEvent>) {
-/// for PacketEvent {
-/// entity,
-/// packet,
-/// } in events.read() {
-/// match packet.as_ref() {
-/// ClientboundGamePacket::LevelParticles(p) => {
-/// // ...
-/// }
-/// _ => {}
-/// }
-/// }
-/// }
-/// ```
-#[derive(Event, Debug, Clone)]
-pub struct PacketEvent {
- /// The client entity that received the packet.
- pub entity: Entity,
- /// The packet that was actually received.
- pub packet: Arc<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 [`ClientboundPlayerCombatKill`] will
-/// be included.
-#[derive(Event, Debug, Clone)]
-pub struct DeathEvent {
- pub entity: Entity,
- pub packet: Option<ClientboundPlayerCombatKill>,
-}
-
-/// A KeepAlive packet is sent from the server to verify that the client is
-/// still connected.
-#[derive(Event, Debug, Clone)]
-pub struct KeepAliveEvent {
- pub entity: Entity,
- /// The ID of the keepalive. This is an arbitrary number, but vanilla
- /// servers use the time to generate this.
- pub id: u64,
-}
-
-#[derive(Event, Debug, Clone)]
-pub struct ResourcePackEvent {
- pub entity: Entity,
- /// The random ID for this request to download the resource pack. The packet
- /// for replying to a resource pack push must contain the same ID.
- pub id: Uuid,
- pub url: String,
- pub hash: String,
- pub required: bool,
- pub prompt: Option<FormattedText>,
-}
-
-/// An instance (aka world, dimension) was loaded by a client.
-///
-/// Since the instance is given to you as a weak reference, it won't be able to
-/// be `upgrade`d if all local players leave it.
-#[derive(Event, Debug, Clone)]
-pub struct InstanceLoadedEvent {
- pub entity: Entity,
- pub name: ResourceLocation,
- pub instance: Weak<RwLock<Instance>>,
-}
-
-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:?}");
- debug!("packet bytes: {raw_packet:?}");
- continue;
- }
- };
- packet_events.send(PacketEvent {
- entity: player_entity,
- packet: Arc::new(packet),
- });
- }
- // clear the packets right after we read them
- packets.clear();
- }
- }
-}
-
-pub fn process_packet_events(ecs: &mut World) {
- let mut events_owned = Vec::<(Entity, Arc<ClientboundGamePacket>)>::new();
- {
- let mut system_state = SystemState::<EventReader<PacketEvent>>::new(ecs);
- let mut events = system_state.get_mut(ecs);
- for PacketEvent {
- entity: player_entity,
- packet,
- } in events.read()
- {
- // we do this so `ecs` isn't borrowed for the whole loop
- events_owned.push((*player_entity, packet.clone()));
- }
- }
- for (player_entity, packet) in events_owned {
- let packet_clone = packet.clone();
- let packet_ref = packet_clone.as_ref();
- match packet_ref {
- ClientboundGamePacket::Login(p) => {
- debug!("Got login packet");
-
- #[allow(clippy::type_complexity)]
- let mut system_state: SystemState<(
- Commands,
- Query<(
- &GameProfileComponent,
- &ClientInformation,
- Option<&mut InstanceName>,
- Option<&mut LoadedBy>,
- &mut EntityIdIndex,
- &mut InstanceHolder,
- )>,
- EventWriter<InstanceLoadedEvent>,
- ResMut<InstanceContainer>,
- ResMut<EntityUuidIndex>,
- EventWriter<SendPacketEvent>,
- )> = SystemState::new(ecs);
- let (
- mut commands,
- mut query,
- mut instance_loaded_events,
- mut instance_container,
- mut entity_uuid_index,
- mut send_packet_events,
- ) = system_state.get_mut(ecs);
- let (
- game_profile,
- client_information,
- instance_name,
- loaded_by,
- mut entity_id_index,
- mut instance_holder,
- ) = query.get_mut(player_entity).unwrap();
-
- {
- let new_instance_name = p.common.dimension.clone();
-
- if let Some(mut instance_name) = instance_name {
- *instance_name = instance_name.clone();
- } else {
- commands
- .entity(player_entity)
- .insert(InstanceName(new_instance_name.clone()));
- }
-
- let Some((_dimension_type, dimension_data)) = p
- .common
- .dimension_type(&instance_holder.instance.read().registries)
- else {
- continue;
- };
-
- // add this world to the instance_container (or don't if it's already
- // there)
- let weak_instance = instance_container.insert(
- new_instance_name.clone(),
- dimension_data.height,
- dimension_data.min_y,
- &instance_holder.instance.read().registries,
- );
- instance_loaded_events.send(InstanceLoadedEvent {
- entity: player_entity,
- name: new_instance_name.clone(),
- instance: Arc::downgrade(&weak_instance),
- });
-
- // set the partial_world to an empty world
- // (when we add chunks or entities those will be in the
- // instance_container)
-
- *instance_holder.partial_instance.write() = PartialInstance::new(
- azalea_world::chunk_storage::calculate_chunk_storage_range(
- client_information.view_distance.into(),
- ),
- // this argument makes it so other clients don't update this player entity
- // in a shared instance
- Some(player_entity),
- );
- {
- let map = instance_holder.instance.read().registries.map.clone();
- let new_registries = &mut weak_instance.write().registries;
- // add the registries from this instance to the weak instance
- for (registry_name, registry) in map {
- new_registries.map.insert(registry_name, registry);
- }
- }
- instance_holder.instance = weak_instance;
-
- let entity_bundle = EntityBundle::new(
- game_profile.uuid,
- Vec3::default(),
- azalea_registry::EntityKind::Player,
- new_instance_name,
- );
- let entity_id = p.player_id;
- // insert our components into the ecs :)
- commands.entity(player_entity).insert((
- entity_id,
- LocalGameMode {
- current: p.common.game_type,
- previous: p.common.previous_game_type.into(),
- },
- entity_bundle,
- ));
-
- azalea_entity::indexing::add_entity_to_indexes(
- entity_id,
- player_entity,
- Some(game_profile.uuid),
- &mut entity_id_index,
- &mut entity_uuid_index,
- &mut instance_holder.instance.write(),
- );
-
- // update or insert loaded_by
- if let Some(mut loaded_by) = loaded_by {
- loaded_by.insert(player_entity);
- } else {
- commands
- .entity(player_entity)
- .insert(LoadedBy(HashSet::from_iter(vec![player_entity])));
- }
- }
-
- // send the client information that we have set
- debug!(
- "Sending client information because login: {:?}",
- client_information
- );
- send_packet_events.send(SendPacketEvent::new(player_entity,
- azalea_protocol::packets::game::s_client_information::ServerboundClientInformation { information: client_information.clone() },
- ));
-
- system_state.apply(ecs);
- }
- ClientboundGamePacket::SetChunkCacheRadius(p) => {
- debug!("Got set chunk cache radius packet {p:?}");
- }
-
- ClientboundGamePacket::ChunkBatchStart(_p) => {
- // the packet is empty, just a marker to tell us when the batch starts and ends
- debug!("Got chunk batch start");
- let mut system_state: SystemState<EventWriter<chunks::ChunkBatchStartEvent>> =
- SystemState::new(ecs);
- let mut chunk_batch_start_events = system_state.get_mut(ecs);
-
- chunk_batch_start_events.send(chunks::ChunkBatchStartEvent {
- entity: player_entity,
- });
- }
- ClientboundGamePacket::ChunkBatchFinished(p) => {
- debug!("Got chunk batch finished {p:?}");
-
- let mut system_state: SystemState<EventWriter<chunks::ChunkBatchFinishedEvent>> =
- SystemState::new(ecs);
- let mut chunk_batch_start_events = system_state.get_mut(ecs);
-
- chunk_batch_start_events.send(chunks::ChunkBatchFinishedEvent {
- entity: player_entity,
- batch_size: p.batch_size,
- });
- }
-
- ClientboundGamePacket::CustomPayload(p) => {
- debug!("Got custom payload packet {p:?}");
- }
- ClientboundGamePacket::ChangeDifficulty(p) => {
- debug!("Got difficulty packet {p:?}");
- }
- ClientboundGamePacket::Commands(_p) => {
- debug!("Got declare commands packet");
- }
- ClientboundGamePacket::PlayerAbilities(p) => {
- debug!("Got player abilities packet {p:?}");
- let mut system_state: SystemState<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::SetCursorItem(p) => {
- debug!("Got set cursor item packet {p:?}");
- }
- ClientboundGamePacket::UpdateTags(_p) => {
- debug!("Got update tags packet");
- }
- ClientboundGamePacket::Disconnect(p) => {
- warn!("Got disconnect packet {p:?}");
- let mut system_state: SystemState<EventWriter<DisconnectEvent>> =
- SystemState::new(ecs);
- let mut disconnect_events = system_state.get_mut(ecs);
- disconnect_events.send(DisconnectEvent {
- entity: player_entity,
- reason: Some(p.reason.clone()),
- });
- }
- ClientboundGamePacket::UpdateRecipes(_p) => {
- debug!("Got update recipes packet");
- }
- ClientboundGamePacket::EntityEvent(_p) => {
- // debug!("Got entity event packet {p:?}");
- }
- ClientboundGamePacket::PlayerPosition(p) => {
- debug!("Got player position packet {p:?}");
-
- #[allow(clippy::type_complexity)]
- let mut system_state: SystemState<(
- Query<(
- &mut Physics,
- &mut LookDirection,
- &mut Position,
- &mut LastSentPosition,
- )>,
- EventWriter<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;
- };
-
- **last_sent_position = **position;
-
- fn apply_change<T: Add<Output = T>>(base: T, condition: bool, change: T) -> T {
- if condition { base + change } else { change }
- }
-
- let new_x = apply_change(position.x, p.relative.x, p.change.pos.x);
- let new_y = apply_change(position.y, p.relative.y, p.change.pos.y);
- let new_z = apply_change(position.z, p.relative.z, p.change.pos.z);
-
- let new_y_rot = apply_change(
- direction.y_rot,
- p.relative.y_rot,
- p.change.look_direction.y_rot,
- );
- let new_x_rot = apply_change(
- direction.x_rot,
- p.relative.x_rot,
- p.change.look_direction.x_rot,
- );
-
- let mut new_delta_from_rotations = physics.velocity;
- if p.relative.rotate_delta {
- let y_rot_delta = direction.y_rot - new_y_rot;
- let x_rot_delta = direction.x_rot - new_x_rot;
- new_delta_from_rotations = new_delta_from_rotations
- .x_rot(math::to_radians(x_rot_delta as f64) as f32)
- .y_rot(math::to_radians(y_rot_delta as f64) as f32);
- }
-
- let new_delta = Vec3::new(
- apply_change(
- new_delta_from_rotations.x,
- p.relative.delta_x,
- p.change.delta.x,
- ),
- apply_change(
- new_delta_from_rotations.y,
- p.relative.delta_y,
- p.change.delta.y,
- ),
- apply_change(
- new_delta_from_rotations.z,
- p.relative.delta_z,
- p.change.delta.z,
- ),
- );
-
- // apply the updates
-
- physics.velocity = new_delta;
-
- (direction.y_rot, direction.x_rot) = (new_y_rot, new_x_rot);
-
- let new_pos = Vec3::new(new_x, new_y, new_z);
- if new_pos != **position {
- **position = new_pos;
- }
-
- // old_pos is set to the current position when we're teleported
- physics.set_old_pos(&position);
-
- // send the relevant packets
-
- send_packet_events.send(SendPacketEvent::new(
- player_entity,
- ServerboundAcceptTeleportation { id: p.id },
- ));
- send_packet_events.send(SendPacketEvent::new(
- player_entity,
- ServerboundMovePlayerPosRot {
- pos: new_pos,
- look_direction: LookDirection::new(new_y_rot, new_x_rot),
- // this is always false
- on_ground: false,
- },
- ));
- }
- ClientboundGamePacket::PlayerInfoUpdate(p) => {
- debug!("Got player info packet {p:?}");
-
- #[allow(clippy::type_complexity)]
- let mut system_state: SystemState<(
- Query<&mut TabList>,
- EventWriter<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.clone_from(&updated_info.display_name);
- }
- update_player_events.send(UpdatePlayerEvent {
- entity: player_entity,
- info: info.clone(),
- });
- } else {
- let uuid = updated_info.profile.uuid;
- #[cfg(debug_assertions)]
- warn!("Ignoring PlayerInfoUpdate for unknown player {uuid}");
- #[cfg(not(debug_assertions))]
- debug!("Ignoring PlayerInfoUpdate for unknown player {uuid}");
- }
- }
-
- *tab_list_resource = tab_list.clone();
- }
- ClientboundGamePacket::PlayerInfoRemove(p) => {
- let mut system_state: SystemState<(
- Query<&mut TabList>,
- EventWriter<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 instance_holder = query.get_mut(player_entity).unwrap();
- let mut partial_world = instance_holder.partial_instance.write();
-
- partial_world
- .chunks
- .update_view_center(ChunkPos::new(p.x, p.z));
- }
- ClientboundGamePacket::ChunksBiomes(_) => {}
- ClientboundGamePacket::LightUpdate(_p) => {
- // debug!("Got light update packet {p:?}");
- }
- ClientboundGamePacket::LevelChunkWithLight(p) => {
- debug!("Got chunk with light packet {} {}", p.x, p.z);
-
- let mut system_state: SystemState<EventWriter<chunks::ReceiveChunkEvent>> =
- SystemState::new(ecs);
- let mut receive_chunk_events = system_state.get_mut(ecs);
- receive_chunk_events.send(chunks::ReceiveChunkEvent {
- entity: player_entity,
- packet: p.clone(),
- });
- }
- ClientboundGamePacket::AddEntity(p) => {
- debug!("Got add entity packet {p:?}");
-
- #[allow(clippy::type_complexity)]
- let mut system_state: SystemState<(
- Commands,
- Query<(&mut EntityIdIndex, Option<&InstanceName>, Option<&TabList>)>,
- Query<&mut LoadedBy>,
- Query<Entity>,
- Res<InstanceContainer>,
- ResMut<EntityUuidIndex>,
- )> = SystemState::new(ecs);
- let (
- mut commands,
- mut query,
- mut loaded_by_query,
- entity_query,
- instance_container,
- mut entity_uuid_index,
- ) = system_state.get_mut(ecs);
- let (mut entity_id_index, instance_name, tab_list) =
- query.get_mut(player_entity).unwrap();
-
- let entity_id = p.id;
-
- let Some(instance_name) = instance_name else {
- warn!("got add player packet but we haven't gotten a login packet yet");
- continue;
- };
-
- // check if the entity already exists, and if it does then only add to LoadedBy
- let instance = instance_container.get(instance_name).unwrap();
- if let Some(&ecs_entity) = instance.read().entity_by_id.get(&entity_id) {
- // entity already exists
- let Ok(mut loaded_by) = loaded_by_query.get_mut(ecs_entity) else {
- // LoadedBy for this entity isn't in the ecs! figure out what went wrong
- // and print an error
-
- let entity_in_ecs = entity_query.get(ecs_entity).is_ok();
-
- if entity_in_ecs {
- error!(
- "LoadedBy for entity {entity_id:?} ({ecs_entity:?}) isn't in the ecs, but the entity is in entity_by_id"
- );
- } else {
- error!(
- "Entity {entity_id:?} ({ecs_entity:?}) isn't in the ecs, but the entity is in entity_by_id"
- );
- }
- continue;
- };
- loaded_by.insert(player_entity);
-
- // per-client id index
- entity_id_index.insert(entity_id, ecs_entity);
-
- debug!("added to LoadedBy of entity {ecs_entity:?} with id {entity_id:?}");
- continue;
- };
-
- // entity doesn't exist in the global index!
-
- let bundle = p.as_entity_bundle((**instance_name).clone());
- let mut spawned =
- commands.spawn((entity_id, LoadedBy(HashSet::from([player_entity])), bundle));
- let ecs_entity: Entity = spawned.id();
- debug!("spawned entity {ecs_entity:?} with id {entity_id:?}");
-
- azalea_entity::indexing::add_entity_to_indexes(
- entity_id,
- ecs_entity,
- Some(p.uuid),
- &mut entity_id_index,
- &mut entity_uuid_index,
- &mut instance.write(),
- );
-
- // add the GameProfileComponent if the uuid is in the tab list
- if let Some(tab_list) = tab_list {
- // (technically this makes it possible for non-player entities to have
- // GameProfileComponents but the server would have to be doing something
- // really weird)
- if let Some(player_info) = tab_list.get(&p.uuid) {
- spawned.insert(GameProfileComponent(player_info.profile.clone()));
- }
- }
-
- // the bundle doesn't include the default entity metadata so we add that
- // separately
- p.apply_metadata(&mut spawned);
-
- system_state.apply(ecs);
- }
- ClientboundGamePacket::SetEntityData(p) => {
- debug!("Got set entity data packet {p:?}");
-
- #[allow(clippy::type_complexity)]
- let mut system_state: SystemState<(
- Commands,
- Query<(&EntityIdIndex, &InstanceHolder)>,
- Query<&EntityKind>,
- )> = SystemState::new(ecs);
- let (mut commands, mut query, entity_kind_query) = system_state.get_mut(ecs);
- let (entity_id_index, instance_holder) = query.get_mut(player_entity).unwrap();
-
- let entity = entity_id_index.get(p.id);
-
- let Some(entity) = entity else {
- // some servers like hypixel trigger this a lot :(
- debug!(
- "Server sent an entity data packet for an entity id ({}) that we don't know about",
- p.id
- );
- continue;
- };
- let entity_kind = *entity_kind_query.get(entity).unwrap();
-
- let packed_items = p.packed_items.clone().to_vec();
-
- // we use RelativeEntityUpdate because it makes sure changes aren't made
- // multiple times
- commands.entity(entity).queue(RelativeEntityUpdate {
- partial_world: instance_holder.partial_instance.clone(),
- update: Box::new(move |entity| {
- let entity_id = entity.id();
- entity.world_scope(|world| {
- let mut commands_system_state = SystemState::<Commands>::new(world);
- let mut commands = commands_system_state.get_mut(world);
- let mut entity_commands = commands.entity(entity_id);
- if let Err(e) =
- apply_metadata(&mut entity_commands, *entity_kind, packed_items)
- {
- warn!("{e}");
- }
- commands_system_state.apply(world);
- });
- }),
- });
-
- system_state.apply(ecs);
- }
- ClientboundGamePacket::UpdateAttributes(_p) => {
- // debug!("Got update attributes packet {p:?}");
- }
- ClientboundGamePacket::SetEntityMotion(p) => {
- // vanilla servers use this packet for knockback, but note that the Explode
- // packet is also sometimes used by servers for knockback
-
- let mut system_state: SystemState<(
- Commands,
- Query<(&EntityIdIndex, &InstanceHolder)>,
- )> = SystemState::new(ecs);
- let (mut commands, mut query) = system_state.get_mut(ecs);
- let (entity_id_index, instance_holder) = query.get_mut(player_entity).unwrap();
-
- let Some(entity) = entity_id_index.get(p.id) else {
- // note that this log (and some other ones like the one in RemoveEntities)
- // sometimes happens when killing mobs. it seems to be a vanilla bug, which is
- // why it's a debug log instead of a warning
- debug!(
- "Got set entity motion packet for unknown entity id {}",
- p.id
- );
- continue;
- };
-
- // this is to make sure the same entity velocity update doesn't get sent
- // multiple times when in swarms
-
- let knockback = KnockbackType::Set(Vec3 {
- x: p.xa as f64 / 8000.,
- y: p.ya as f64 / 8000.,
- z: p.za as f64 / 8000.,
- });
-
- commands.entity(entity).queue(RelativeEntityUpdate {
- partial_world: instance_holder.partial_instance.clone(),
- update: Box::new(move |entity_mut| {
- entity_mut.world_scope(|world| {
- world.send_event(KnockbackEvent { entity, knockback })
- });
- }),
- });
-
- system_state.apply(ecs);
- }
- ClientboundGamePacket::SetEntityLink(p) => {
- debug!("Got set entity link packet {p:?}");
- }
- ClientboundGamePacket::InitializeBorder(p) => {
- debug!("Got initialize border packet {p:?}");
- }
- ClientboundGamePacket::SetTime(_p) => {
- // debug!("Got set time packet {p:?}");
- }
- ClientboundGamePacket::SetDefaultSpawnPosition(p) => {
- debug!("Got set default spawn position packet {p:?}");
- }
- ClientboundGamePacket::SetHealth(p) => {
- debug!("Got set health packet {p:?}");
-
- let mut system_state: SystemState<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 Some(entity) = entity_id_index.get(p.id) else {
- warn!("Got teleport entity packet for unknown entity id {}", p.id);
- continue;
- };
-
- let new_pos = p.change.pos;
- let new_look_direction = LookDirection {
- x_rot: (p.change.look_direction.x_rot as i32 * 360) as f32 / 256.,
- y_rot: (p.change.look_direction.y_rot as i32 * 360) as f32 / 256.,
- };
- commands.entity(entity).queue(RelativeEntityUpdate {
- partial_world: instance_holder.partial_instance.clone(),
- update: Box::new(move |entity| {
- let mut position = entity.get_mut::<Position>().unwrap();
- if new_pos != **position {
- **position = new_pos;
- }
- let position = *position;
- let mut look_direction = entity.get_mut::<LookDirection>().unwrap();
- if new_look_direction != *look_direction {
- *look_direction = new_look_direction;
- }
- // old_pos is set to the current position when we're teleported
- let mut physics = entity.get_mut::<Physics>().unwrap();
- physics.set_old_pos(&position);
- }),
- });
-
- system_state.apply(ecs);
- }
- ClientboundGamePacket::UpdateAdvancements(p) => {
- debug!("Got update advancements packet {p:?}");
- }
- ClientboundGamePacket::RotateHead(_p) => {
- // debug!("Got rotate head packet {p:?}");
- }
- ClientboundGamePacket::MoveEntityPos(p) => {
- let mut system_state: SystemState<(
- Commands,
- Query<(&EntityIdIndex, &InstanceHolder)>,
- )> = SystemState::new(ecs);
- let (mut commands, mut query) = system_state.get_mut(ecs);
- let (entity_id_index, instance_holder) = query.get_mut(player_entity).unwrap();
-
- debug!("Got move entity pos packet {p:?}");
-
- let Some(entity) = entity_id_index.get(p.entity_id) else {
- debug!(
- "Got move entity pos packet for unknown entity id {}",
- p.entity_id
- );
- continue;
- };
-
- let new_delta = p.delta.clone();
- let new_on_ground = p.on_ground;
- commands.entity(entity).queue(RelativeEntityUpdate {
- partial_world: instance_holder.partial_instance.clone(),
- update: Box::new(move |entity_mut| {
- let mut physics = entity_mut.get_mut::<Physics>().unwrap();
- let new_pos = physics.vec_delta_codec.decode(
- new_delta.xa as i64,
- new_delta.ya as i64,
- new_delta.za as i64,
- );
- physics.vec_delta_codec.set_base(new_pos);
- physics.set_on_ground(new_on_ground);
-
- let mut position = entity_mut.get_mut::<Position>().unwrap();
- if new_pos != **position {
- **position = new_pos;
- }
- }),
- });
-
- system_state.apply(ecs);
- }
- ClientboundGamePacket::MoveEntityPosRot(p) => {
- let mut system_state: SystemState<(
- Commands,
- Query<(&EntityIdIndex, &InstanceHolder)>,
- )> = SystemState::new(ecs);
- let (mut commands, mut query) = system_state.get_mut(ecs);
- let (entity_id_index, instance_holder) = query.get_mut(player_entity).unwrap();
-
- debug!("Got move entity pos rot packet {p:?}");
-
- let entity = entity_id_index.get(p.entity_id);
-
- if let Some(entity) = entity {
- let new_delta = p.delta.clone();
- let new_look_direction = LookDirection {
- x_rot: (p.x_rot as i32 * 360) as f32 / 256.,
- y_rot: (p.y_rot as i32 * 360) as f32 / 256.,
- };
-
- let new_on_ground = p.on_ground;
-
- commands.entity(entity).queue(RelativeEntityUpdate {
- partial_world: instance_holder.partial_instance.clone(),
- update: Box::new(move |entity_mut| {
- let mut physics = entity_mut.get_mut::<Physics>().unwrap();
- let new_pos = physics.vec_delta_codec.decode(
- new_delta.xa as i64,
- new_delta.ya as i64,
- new_delta.za as i64,
- );
- physics.vec_delta_codec.set_base(new_pos);
- physics.set_on_ground(new_on_ground);
-
- let mut position = entity_mut.get_mut::<Position>().unwrap();
- if new_pos != **position {
- **position = new_pos;
- }
-
- let mut look_direction = entity_mut.get_mut::<LookDirection>().unwrap();
- if new_look_direction != *look_direction {
- *look_direction = new_look_direction;
- }
- }),
- });
- } else {
- // often triggered by hypixel :(
- debug!(
- "Got move entity pos rot packet for unknown entity id {}",
- p.entity_id
- );
- }
-
- system_state.apply(ecs);
- }
-
- ClientboundGamePacket::MoveEntityRot(p) => {
- let mut system_state: SystemState<(
- Commands,
- Query<(&EntityIdIndex, &InstanceHolder)>,
- )> = SystemState::new(ecs);
- let (mut commands, mut query) = system_state.get_mut(ecs);
- let (entity_id_index, instance_holder) = query.get_mut(player_entity).unwrap();
-
- let entity = entity_id_index.get(p.entity_id);
-
- if let Some(entity) = entity {
- let new_look_direction = LookDirection {
- x_rot: (p.x_rot as i32 * 360) as f32 / 256.,
- y_rot: (p.y_rot as i32 * 360) as f32 / 256.,
- };
- let new_on_ground = p.on_ground;
-
- commands.entity(entity).queue(RelativeEntityUpdate {
- partial_world: instance_holder.partial_instance.clone(),
- update: Box::new(move |entity_mut| {
- let mut physics = entity_mut.get_mut::<Physics>().unwrap();
- physics.set_on_ground(new_on_ground);
-
- let mut look_direction = entity_mut.get_mut::<LookDirection>().unwrap();
- if new_look_direction != *look_direction {
- *look_direction = new_look_direction;
- }
- }),
- });
- } else {
- warn!(
- "Got move entity rot packet for unknown entity id {}",
- p.entity_id
- );
- }
-
- system_state.apply(ecs);
- }
- 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::new(
- player_entity,
- ServerboundKeepAlive { id: p.id },
- ));
- }
- ClientboundGamePacket::RemoveEntities(p) => {
- debug!("Got remove entities packet {p:?}");
-
- let mut system_state: SystemState<(
- Query<&mut EntityIdIndex>,
- Query<&mut LoadedBy>,
- )> = SystemState::new(ecs);
-
- let (mut query, mut entity_query) = system_state.get_mut(ecs);
- let Ok(mut entity_id_index) = query.get_mut(player_entity) else {
- warn!("our local player doesn't have EntityIdIndex");
- continue;
- };
-
- for &id in &p.entity_ids {
- let Some(entity) = entity_id_index.remove(id) else {
- debug!(
- "Tried to remove entity with id {id} but it wasn't in the EntityIdIndex"
- );
- continue;
- };
- let Ok(mut loaded_by) = entity_query.get_mut(entity) else {
- warn!(
- "tried to despawn entity {id} but it doesn't have a LoadedBy component",
- );
- continue;
- };
-
- // the [`remove_despawned_entities_from_indexes`] system will despawn the entity
- // if it's not loaded by anything anymore
-
- // also we can't just ecs.despawn because if we're in a swarm then the entity
- // might still be loaded by another client
-
- loaded_by.remove(&player_entity);
- }
- }
- ClientboundGamePacket::PlayerChat(p) => {
- debug!("Got player chat packet {p:?}");
-
- let mut system_state: SystemState<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::DisguisedChat(p) => {
- debug!("Got disguised 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::Disguised(Arc::new(p.clone())),
- });
- }
- ClientboundGamePacket::Sound(_p) => {
- // debug!("Got sound packet {p:?}");
- }
- ClientboundGamePacket::LevelEvent(p) => {
- debug!("Got level event packet {p:?}");
- }
- ClientboundGamePacket::BlockUpdate(p) => {
- debug!("Got block update packet {p:?}");
-
- let mut system_state: SystemState<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::c_game_event::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 Inventory>,
- 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,
- });
- }
- }
- 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 Inventory>> = SystemState::new(ecs);
- let mut query = system_state.get_mut(ecs);
- let mut inventory = query.get_mut(player_entity).unwrap();
-
- if p.container_id == -1 {
- // -1 means carried item
- inventory.carried = p.item_stack.clone();
- } else if p.container_id == -2 {
- if let Some(slot) = inventory.inventory_menu.slot_mut(p.slot.into()) {
- *slot = p.item_stack.clone();
- }
- } else {
- let is_creative_mode_and_inventory_closed = false;
- // technically minecraft has slightly different behavior here if you're in
- // creative mode and have your inventory open
- if p.container_id == 0
- && azalea_inventory::Player::is_hotbar_slot(p.slot.into())
- {
- // minecraft also sets a "pop time" here which is used for an animation
- // but that's not really necessary
- if let Some(slot) = inventory.inventory_menu.slot_mut(p.slot.into()) {
- *slot = p.item_stack.clone();
- }
- } else if p.container_id == inventory.id
- && (p.container_id != 0 || !is_creative_mode_and_inventory_closed)
- {
- // var2.containerMenu.setItem(var4, var1.getStateId(), var3);
- if let Some(slot) = inventory.menu_mut().slot_mut(p.slot.into()) {
- *slot = p.item_stack.clone();
- inventory.state_id = p.state_id;
- }
- }
- }
- }
- ClientboundGamePacket::ContainerClose(_p) => {
- // there's p.container_id but minecraft doesn't actually check it
- let mut system_state: SystemState<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(p) => {
- trace!("Got explode packet {p:?}");
- if let Some(knockback) = p.knockback {
- let mut system_state: SystemState<EventWriter<KnockbackEvent>> =
- SystemState::new(ecs);
- let mut knockback_events = system_state.get_mut(ecs);
-
- knockback_events.send(KnockbackEvent {
- entity: player_entity,
- knockback: KnockbackType::Set(knockback),
- });
-
- system_state.apply(ecs);
- }
- }
- ClientboundGamePacket::ForgetLevelChunk(p) => {
- debug!("Got forget level chunk packet {p:?}");
-
- let mut system_state: SystemState<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_instance = local_player.partial_instance.write();
-
- partial_instance.chunks.limited_set(&p.pos, None);
- }
- ClientboundGamePacket::HorseScreenOpen(_) => {}
- ClientboundGamePacket::MapItemData(_) => {}
- ClientboundGamePacket::MerchantOffers(_) => {}
- ClientboundGamePacket::MoveVehicle(_) => {}
- ClientboundGamePacket::OpenBook(_) => {}
- ClientboundGamePacket::OpenScreen(p) => {
- debug!("Got open screen packet {p:?}");
- let mut system_state: SystemState<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.to_owned(),
- });
- }
- 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::new(
- player_entity,
- ServerboundPong { id: p.id },
- ));
- }
- ClientboundGamePacket::PlaceGhostRecipe(_) => {}
- ClientboundGamePacket::PlayerCombatEnd(_) => {}
- ClientboundGamePacket::PlayerCombatEnter(_) => {}
- ClientboundGamePacket::PlayerCombatKill(p) => {
- debug!("Got player kill packet {p:?}");
-
- #[allow(clippy::type_complexity)]
- let mut system_state: SystemState<(
- Commands,
- Query<(&MinecraftEntityId, Option<&Dead>)>,
- EventWriter<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::ResourcePackPush(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,
- id: p.id,
- url: p.url.to_owned(),
- hash: p.hash.to_owned(),
- required: p.required,
- prompt: p.prompt.to_owned(),
- });
-
- system_state.apply(ecs);
- }
- ClientboundGamePacket::ResourcePackPop(_) => {}
- ClientboundGamePacket::Respawn(p) => {
- debug!("Got respawn packet {p:?}");
-
- #[allow(clippy::type_complexity)]
- let mut system_state: SystemState<(
- Commands,
- Query<(
- &mut InstanceHolder,
- &GameProfileComponent,
- &ClientInformation,
- )>,
- EventWriter<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) =
- query.get_mut(player_entity).unwrap();
-
- {
- let new_instance_name = p.common.dimension.clone();
-
- let Some((_dimension_type, dimension_data)) = p
- .common
- .dimension_type(&instance_holder.instance.read().registries)
- else {
- continue;
- };
-
- // add this world to the instance_container (or don't if it's already
- // there)
- let weak_instance = instance_container.insert(
- new_instance_name.clone(),
- dimension_data.height,
- dimension_data.min_y,
- &instance_holder.instance.read().registries,
- );
- instance_loaded_events.send(InstanceLoadedEvent {
- entity: player_entity,
- name: new_instance_name.clone(),
- instance: Arc::downgrade(&weak_instance),
- });
-
- // set the partial_world to an empty world
- // (when we add chunks or entities those will be in the
- // instance_container)
-
- *instance_holder.partial_instance.write() = PartialInstance::new(
- azalea_world::chunk_storage::calculate_chunk_storage_range(
- client_information.view_distance.into(),
- ),
- Some(player_entity),
- );
- instance_holder.instance = weak_instance;
-
- // this resets a bunch of our components like physics and stuff
- let entity_bundle = EntityBundle::new(
- game_profile.uuid,
- Vec3::default(),
- azalea_registry::EntityKind::Player,
- new_instance_name,
- );
- // update the local gamemode and metadata things
- commands.entity(player_entity).insert((
- LocalGameMode {
- current: p.common.game_type,
- previous: p.common.previous_game_type.into(),
- },
- entity_bundle,
- ));
- }
-
- // Remove the Dead marker component from the player.
- commands.entity(player_entity).remove::<Dead>();
-
- system_state.apply(ecs);
- }
-
- ClientboundGamePacket::StartConfiguration(_p) => {
- let mut system_state: SystemState<(Commands, EventWriter<SendPacketEvent>)> =
- SystemState::new(ecs);
- let (mut commands, mut packet_events) = system_state.get_mut(ecs);
-
- packet_events.send(SendPacketEvent::new(
- player_entity,
- ServerboundConfigurationAcknowledged {},
- ));
-
- commands
- .entity(player_entity)
- .insert(crate::client::InConfigState)
- .remove::<crate::JoinedClientBundle>();
-
- system_state.apply(ecs);
- }
-
- ClientboundGamePacket::EntityPositionSync(p) => {
- let mut system_state: SystemState<(
- Commands,
- Query<(&EntityIdIndex, &InstanceHolder)>,
- )> = SystemState::new(ecs);
- let (mut commands, mut query) = system_state.get_mut(ecs);
- let (entity_id_index, instance_holder) = query.get_mut(player_entity).unwrap();
-
- let Some(entity) = entity_id_index.get(p.id) else {
- debug!("Got teleport entity packet for unknown entity id {}", p.id);
- continue;
- };
-
- let new_position = p.values.pos;
- let new_on_ground = p.on_ground;
- let new_look_direction = p.values.look_direction;
-
- commands.entity(entity).queue(RelativeEntityUpdate {
- partial_world: instance_holder.partial_instance.clone(),
- update: Box::new(move |entity_mut| {
- let is_local_entity = entity_mut.get::<LocalEntity>().is_some();
- let mut physics = entity_mut.get_mut::<Physics>().unwrap();
-
- physics.vec_delta_codec.set_base(new_position);
-
- if is_local_entity {
- debug!("Ignoring entity position sync packet for local player");
- return;
- }
-
- physics.set_on_ground(new_on_ground);
-
- let mut last_sent_position =
- entity_mut.get_mut::<LastSentPosition>().unwrap();
- **last_sent_position = new_position;
- let mut position = entity_mut.get_mut::<Position>().unwrap();
- **position = new_position;
-
- let mut look_direction = entity_mut.get_mut::<LookDirection>().unwrap();
- *look_direction = new_look_direction;
- }),
- });
-
- system_state.apply(ecs);
- }
-
- ClientboundGamePacket::SelectAdvancementsTab(_) => {}
- ClientboundGamePacket::SetActionBarText(_) => {}
- ClientboundGamePacket::SetBorderCenter(_) => {}
- ClientboundGamePacket::SetBorderLerpSize(_) => {}
- ClientboundGamePacket::SetBorderSize(_) => {}
- ClientboundGamePacket::SetBorderWarningDelay(_) => {}
- ClientboundGamePacket::SetBorderWarningDistance(_) => {}
- ClientboundGamePacket::SetCamera(_) => {}
- ClientboundGamePacket::SetDisplayObjective(_) => {}
- ClientboundGamePacket::SetObjective(_) => {}
- ClientboundGamePacket::SetPassengers(_) => {}
- ClientboundGamePacket::SetPlayerTeam(_) => {}
- ClientboundGamePacket::SetScore(_) => {}
- ClientboundGamePacket::SetSimulationDistance(_) => {}
- ClientboundGamePacket::SetSubtitleText(_) => {}
- ClientboundGamePacket::SetTitleText(_) => {}
- ClientboundGamePacket::SetTitlesAnimation(_) => {}
- ClientboundGamePacket::ClearTitles(_) => {}
- ClientboundGamePacket::SoundEntity(_) => {}
- ClientboundGamePacket::StopSound(_) => {}
- ClientboundGamePacket::TabList(_) => {}
- ClientboundGamePacket::TagQuery(_) => {}
- ClientboundGamePacket::TakeItemEntity(_) => {}
- ClientboundGamePacket::BundleDelimiter(_) => {}
- ClientboundGamePacket::DamageEvent(_) => {}
- ClientboundGamePacket::HurtAnimation(_) => {}
-
- ClientboundGamePacket::TickingState(_) => {}
- ClientboundGamePacket::TickingStep(_) => {}
-
- ClientboundGamePacket::ResetScore(_) => {}
- ClientboundGamePacket::CookieRequest(_) => {}
- ClientboundGamePacket::DebugSample(_) => {}
- ClientboundGamePacket::PongResponse(_) => {}
- ClientboundGamePacket::StoreCookie(_) => {}
- ClientboundGamePacket::Transfer(_) => {}
- ClientboundGamePacket::MoveMinecartAlongTrack(_) => {}
- ClientboundGamePacket::SetHeldSlot(_) => {}
- ClientboundGamePacket::SetPlayerInventory(_) => {}
- ClientboundGamePacket::ProjectilePower(_) => {}
- ClientboundGamePacket::CustomReportDetails(_) => {}
- ClientboundGamePacket::ServerLinks(_) => {}
- ClientboundGamePacket::PlayerRotation(_) => {}
- ClientboundGamePacket::RecipeBookAdd(_) => {}
- ClientboundGamePacket::RecipeBookRemove(_) => {}
- ClientboundGamePacket::RecipeBookSettings(_) => {}
- }
- }
-}
-
-/// An event for sending a packet to the server while we're in the `game` state.
-#[derive(Event)]
-pub struct SendPacketEvent {
- pub sent_by: Entity,
- pub packet: ServerboundGamePacket,
-}
-impl SendPacketEvent {
- pub fn new(sent_by: Entity, packet: impl Packet<ServerboundGamePacket>) -> Self {
- let packet = packet.into_variant();
- Self { sent_by, packet }
- }
-}
-
-pub fn handle_send_packet_event(
- mut send_packet_events: EventReader<SendPacketEvent>,
- mut query: Query<&mut RawConnection>,
-) {
- for event in send_packet_events.read() {
- if let Ok(raw_connection) = query.get_mut(event.sent_by) {
- // debug!("Sending packet: {:?}", event.packet);
- if let Err(e) = raw_connection.write_packet(event.packet.clone()) {
- error!("Failed to send packet: {e}");
- }
- }
- }
-}
diff --git a/azalea-client/src/player.rs b/azalea-client/src/player.rs
index f0641cf1..0940255c 100755
--- a/azalea-client/src/player.rs
+++ b/azalea-client/src/player.rs
@@ -8,7 +8,7 @@ use bevy_ecs::{
};
use uuid::Uuid;
-use crate::{GameProfileComponent, packet_handling::game::AddPlayerEvent};
+use crate::{GameProfileComponent, packet::game::AddPlayerEvent};
/// A player in the tab list.
#[derive(Debug, Clone)]
diff --git a/azalea-client/src/attack.rs b/azalea-client/src/plugins/attack.rs
index 0f5a8305..1b2bc1ee 100644
--- a/azalea-client/src/attack.rs
+++ b/azalea-client/src/plugins/attack.rs
@@ -11,9 +11,10 @@ use bevy_app::{App, Plugin, Update};
use bevy_ecs::prelude::*;
use derive_more::{Deref, DerefMut};
+use super::packet::game::SendPacketEvent;
use crate::{
Client, interact::SwingArmEvent, local_player::LocalGameMode, movement::MoveEventsSet,
- packet_handling::game::SendPacketEvent, respawn::perform_respawn,
+ respawn::perform_respawn,
};
pub struct AttackPlugin;
diff --git a/azalea-client/src/configuration.rs b/azalea-client/src/plugins/brand.rs
index d578be7a..e15a6c67 100644
--- a/azalea-client/src/configuration.rs
+++ b/azalea-client/src/plugins/brand.rs
@@ -11,15 +11,15 @@ use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use tracing::{debug, warn};
-use crate::packet_handling::{configuration::SendConfigurationEvent, login::InLoginState};
+use super::packet::config::SendConfigPacketEvent;
+use crate::packet::login::InLoginState;
-pub struct ConfigurationPlugin;
-impl Plugin for ConfigurationPlugin {
+pub struct BrandPlugin;
+impl Plugin for BrandPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
Update,
- handle_end_login_state
- .before(crate::packet_handling::configuration::handle_send_packet_event),
+ handle_end_login_state.before(crate::packet::config::handle_send_packet_event),
);
}
}
@@ -27,13 +27,14 @@ impl Plugin for ConfigurationPlugin {
fn handle_end_login_state(
mut removed: RemovedComponents<InLoginState>,
query: Query<&ClientInformation>,
- mut send_packet_events: EventWriter<SendConfigurationEvent>,
+ mut send_packet_events: EventWriter<SendConfigPacketEvent>,
) {
for entity in removed.read() {
let mut brand_data = Vec::new();
- // they don't have to know :)
+ // azalea pretends to be vanilla everywhere else so it makes sense to lie here
+ // too
"vanilla".azalea_write(&mut brand_data).unwrap();
- send_packet_events.send(SendConfigurationEvent::new(
+ send_packet_events.send(SendConfigPacketEvent::new(
entity,
ServerboundCustomPayload {
identifier: ResourceLocation::new("brand"),
@@ -52,7 +53,7 @@ fn handle_end_login_state(
};
debug!("Writing ClientInformation while in config state: {client_information:?}");
- send_packet_events.send(SendConfigurationEvent::new(
+ send_packet_events.send(SendConfigPacketEvent::new(
entity,
ServerboundClientInformation {
information: client_information.clone(),
diff --git a/azalea-client/src/plugins/chat/handler.rs b/azalea-client/src/plugins/chat/handler.rs
new file mode 100644
index 00000000..d598acdb
--- /dev/null
+++ b/azalea-client/src/plugins/chat/handler.rs
@@ -0,0 +1,61 @@
+use std::time::{SystemTime, UNIX_EPOCH};
+
+use azalea_protocol::packets::{
+ game::{s_chat::LastSeenMessagesUpdate, ServerboundChat, ServerboundChatCommand},
+ Packet,
+};
+use bevy_ecs::prelude::*;
+
+use super::ChatKind;
+use crate::packet::game::SendPacketEvent;
+
+/// Send a chat packet to the server of a specific kind (chat message or
+/// command). Usually you just want [`SendChatEvent`] instead.
+///
+/// Usually setting the kind to `Message` will make it send a chat message even
+/// if it starts with a slash, but some server implementations will always do a
+/// command if it starts with a slash.
+///
+/// If you're wondering why this isn't two separate events, it's so ordering is
+/// preserved if multiple chat messages and commands are sent at the same time.
+#[derive(Event)]
+pub struct SendChatKindEvent {
+ pub entity: Entity,
+ pub content: String,
+ pub kind: ChatKind,
+}
+
+pub fn handle_send_chat_kind_event(
+ mut events: EventReader<SendChatKindEvent>,
+ mut send_packet_events: EventWriter<SendPacketEvent>,
+) {
+ for event in events.read() {
+ let content = event
+ .content
+ .chars()
+ .filter(|c| !matches!(c, '\x00'..='\x1F' | '\x7F' | '§'))
+ .take(256)
+ .collect::<String>();
+ let packet = match event.kind {
+ ChatKind::Message => ServerboundChat {
+ message: content,
+ timestamp: SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .expect("Time shouldn't be before epoch")
+ .as_millis()
+ .try_into()
+ .expect("Instant should fit into a u64"),
+ salt: azalea_crypto::make_salt(),
+ signature: None,
+ last_seen_messages: LastSeenMessagesUpdate::default(),
+ }
+ .into_variant(),
+ ChatKind::Command => {
+ // TODO: chat signing
+ ServerboundChatCommand { command: content }.into_variant()
+ }
+ };
+
+ send_packet_events.send(SendPacketEvent::new(event.entity, packet));
+ }
+}
diff --git a/azalea-client/src/chat.rs b/azalea-client/src/plugins/chat/mod.rs
index 2bef9570..66c77b56 100755..100644
--- a/azalea-client/src/chat.rs
+++ b/azalea-client/src/plugins/chat/mod.rs
@@ -1,20 +1,13 @@
//! Implementations of chat-related features.
-use std::{
- sync::Arc,
- time::{SystemTime, UNIX_EPOCH},
-};
+pub mod handler;
+
+use std::sync::Arc;
use azalea_chat::FormattedText;
-use azalea_protocol::packets::{
- Packet,
- game::{
- c_disguised_chat::ClientboundDisguisedChat,
- c_player_chat::ClientboundPlayerChat,
- c_system_chat::ClientboundSystemChat,
- s_chat::{LastSeenMessagesUpdate, ServerboundChat},
- s_chat_command::ServerboundChatCommand,
- },
+use azalea_protocol::packets::game::{
+ c_disguised_chat::ClientboundDisguisedChat, c_player_chat::ClientboundPlayerChat,
+ c_system_chat::ClientboundSystemChat,
};
use bevy_app::{App, Plugin, Update};
use bevy_ecs::{
@@ -23,12 +16,28 @@ use bevy_ecs::{
prelude::Event,
schedule::IntoSystemConfigs,
};
+use handler::{SendChatKindEvent, handle_send_chat_kind_event};
use uuid::Uuid;
-use crate::{
- client::Client,
- packet_handling::game::{SendPacketEvent, handle_send_packet_event},
-};
+use super::packet::game::handle_outgoing_packets;
+use crate::client::Client;
+
+pub struct ChatPlugin;
+impl Plugin for ChatPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_event::<SendChatEvent>()
+ .add_event::<SendChatKindEvent>()
+ .add_event::<ChatReceivedEvent>()
+ .add_systems(
+ Update,
+ (
+ handle_send_chat_event,
+ handle_send_chat_kind_event.after(handle_outgoing_packets),
+ )
+ .chain(),
+ );
+ }
+}
/// A chat packet, either a system message or a chat message.
#[derive(Debug, Clone, PartialEq)]
@@ -183,23 +192,6 @@ impl Client {
}
}
-pub struct ChatPlugin;
-impl Plugin for ChatPlugin {
- fn build(&self, app: &mut App) {
- app.add_event::<SendChatEvent>()
- .add_event::<SendChatKindEvent>()
- .add_event::<ChatReceivedEvent>()
- .add_systems(
- Update,
- (
- handle_send_chat_event,
- handle_send_chat_kind_event.after(handle_send_packet_event),
- )
- .chain(),
- );
- }
-}
-
/// A client received a chat message packet.
#[derive(Event, Debug, Clone)]
pub struct ChatReceivedEvent {
@@ -235,63 +227,12 @@ pub fn handle_send_chat_event(
}
}
-/// Send a chat packet to the server of a specific kind (chat message or
-/// command). Usually you just want [`SendChatEvent`] instead.
-///
-/// Usually setting the kind to `Message` will make it send a chat message even
-/// if it starts with a slash, but some server implementations will always do a
-/// command if it starts with a slash.
-///
-/// If you're wondering why this isn't two separate events, it's so ordering is
-/// preserved if multiple chat messages and commands are sent at the same time.
-#[derive(Event)]
-pub struct SendChatKindEvent {
- pub entity: Entity,
- pub content: String,
- pub kind: ChatKind,
-}
-
/// A kind of chat packet, either a chat message or a command.
pub enum ChatKind {
Message,
Command,
}
-pub fn handle_send_chat_kind_event(
- mut events: EventReader<SendChatKindEvent>,
- mut send_packet_events: EventWriter<SendPacketEvent>,
-) {
- for event in events.read() {
- let content = event
- .content
- .chars()
- .filter(|c| !matches!(c, '\x00'..='\x1F' | '\x7F' | '§'))
- .take(256)
- .collect::<String>();
- let packet = match event.kind {
- ChatKind::Message => ServerboundChat {
- message: content,
- timestamp: SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .expect("Time shouldn't be before epoch")
- .as_millis()
- .try_into()
- .expect("Instant should fit into a u64"),
- salt: azalea_crypto::make_salt(),
- signature: None,
- last_seen_messages: LastSeenMessagesUpdate::default(),
- }
- .into_variant(),
- ChatKind::Command => {
- // TODO: chat signing
- ServerboundChatCommand { command: content }.into_variant()
- }
- };
-
- send_packet_events.send(SendPacketEvent::new(event.entity, packet));
- }
-}
-
// TODO
// MessageSigner, ChatMessageContent, LastSeenMessages
// fn sign_message() -> MessageSignature {
diff --git a/azalea-client/src/chunks.rs b/azalea-client/src/plugins/chunks.rs
index 67313757..cdda3eba 100644
--- a/azalea-client/src/chunks.rs
+++ b/azalea-client/src/plugins/chunks.rs
@@ -18,16 +18,14 @@ use bevy_ecs::prelude::*;
use simdnbt::owned::BaseNbt;
use tracing::{error, trace};
+use super::packet::game::handle_outgoing_packets;
use crate::{
- InstanceHolder,
- interact::handle_block_interact_event,
- inventory::InventorySet,
- packet_handling::game::{SendPacketEvent, handle_send_packet_event},
- respawn::perform_respawn,
+ InstanceHolder, interact::handle_block_interact_event, inventory::InventorySet,
+ packet::game::SendPacketEvent, respawn::perform_respawn,
};
-pub struct ChunkPlugin;
-impl Plugin for ChunkPlugin {
+pub struct ChunksPlugin;
+impl Plugin for ChunksPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
Update,
@@ -37,7 +35,7 @@ impl Plugin for ChunkPlugin {
handle_chunk_batch_finished_event,
)
.chain()
- .before(handle_send_packet_event)
+ .before(handle_outgoing_packets)
.before(InventorySet)
.before(handle_block_interact_event)
.before(perform_respawn),
diff --git a/azalea-client/src/disconnect.rs b/azalea-client/src/plugins/disconnect.rs
index bd10ac75..bd10ac75 100644
--- a/azalea-client/src/disconnect.rs
+++ b/azalea-client/src/plugins/disconnect.rs
diff --git a/azalea-client/src/events.rs b/azalea-client/src/plugins/events.rs
index aed16bcb..3d34d75f 100644
--- a/azalea-client/src/events.rs
+++ b/azalea-client/src/plugins/events.rs
@@ -5,6 +5,7 @@ use std::sync::Arc;
use azalea_chat::FormattedText;
use azalea_core::tick::GameTick;
+use azalea_entity::Dead;
use azalea_protocol::packets::game::{
ClientboundGamePacket, c_player_combat_kill::ClientboundPlayerCombatKill,
};
@@ -24,28 +25,33 @@ use crate::{
PlayerInfo,
chat::{ChatPacket, ChatReceivedEvent},
disconnect::DisconnectEvent,
- packet_handling::game::{
- AddPlayerEvent, DeathEvent, KeepAliveEvent, PacketEvent, RemovePlayerEvent,
+ packet::game::{
+ AddPlayerEvent, DeathEvent, KeepAliveEvent, ReceivePacketEvent, RemovePlayerEvent,
UpdatePlayerEvent,
},
};
// (for contributors):
// HOW TO ADD A NEW (packet based) EVENT:
-// - make a struct that contains an entity field and a data field (look in
-// packet_handling.rs for examples, also you should end the struct name with
-// "Event")
-// - the entity field is the local player entity that's receiving the event
-// - in packet_handling, you always have a variable called player_entity that
-// you can use
-// - add the event struct in the `impl Plugin for PacketHandlerPlugin`
-// - to get the event writer, you have to get an
-// EventWriter<SomethingHappenedEvent> from the SystemState (the convention is
-// to end your variable with the word "events", like "something_events")
+// - Add it as an ECS event first:
+// - Make a struct that contains an entity field and some data fields (look
+// in packet/game/events.rs for examples. These structs should always have
+// their names end with "Event".
+// - (the `entity` field is the local player entity that's receiving the
+// event)
+// - In the GamePacketHandler, you always have a `player` field that you can
+// use.
+// - Add the event struct in PacketPlugin::build
+// - (in the `impl Plugin for PacketPlugin`)
+// - To get the event writer, you have to get an EventWriter<ThingEvent>.
+// Look at other packets in packet/game/mod.rs for examples.
//
-// - then here in this file, add it to the Event enum
-// - and make an event listener system/function like the other ones and put the
-// function in the `impl Plugin for EventPlugin`
+// At this point, you've created a new ECS event. That's annoying for bots to
+// use though, so you might wanna add it to the Event enum too:
+// - In this file, add a new variant to that Event enum with the same name
+// as your event (without the "Event" suffix).
+// - Create a new system function like the other ones here, and put that
+// system function in the `impl Plugin for EventsPlugin`
/// Something that happened in-game, such as a tick passing or chat message
/// being sent.
@@ -111,8 +117,8 @@ pub enum Event {
#[derive(Component, Deref, DerefMut)]
pub struct LocalPlayerEvents(pub mpsc::UnboundedSender<Event>);
-pub struct EventPlugin;
-impl Plugin for EventPlugin {
+pub struct EventsPlugin;
+impl Plugin for EventsPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
Update,
@@ -130,7 +136,7 @@ impl Plugin for EventPlugin {
)
.add_systems(
PreUpdate,
- init_listener.before(crate::packet_handling::game::process_packet_events),
+ init_listener.before(crate::packet::game::process_packet_events),
)
.add_systems(GameTick, tick_listener);
}
@@ -166,7 +172,10 @@ pub fn tick_listener(query: Query<&LocalPlayerEvents, With<InstanceName>>) {
}
}
-pub fn packet_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader<PacketEvent>) {
+pub fn packet_listener(
+ query: Query<&LocalPlayerEvents>,
+ mut events: EventReader<ReceivePacketEvent>,
+) {
for event in events.read() {
let local_player_events = query
.get(event.entity)
@@ -219,6 +228,13 @@ pub fn death_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader<
}
}
+/// Send the "Death" event for [`LocalEntity`]s that died with no reason.
+pub fn dead_component_listener(query: Query<&LocalPlayerEvents, Added<Dead>>) {
+ for local_player_events in &query {
+ local_player_events.send(Event::Death(None)).unwrap();
+ }
+}
+
pub fn keepalive_listener(
query: Query<&LocalPlayerEvents>,
mut events: EventReader<KeepAliveEvent>,
diff --git a/azalea-client/src/interact.rs b/azalea-client/src/plugins/interact.rs
index fdeff197..1a344cc8 100644
--- a/azalea-client/src/interact.rs
+++ b/azalea-client/src/plugins/interact.rs
@@ -30,13 +30,14 @@ use bevy_ecs::{
use derive_more::{Deref, DerefMut};
use tracing::warn;
+use super::packet::game::handle_outgoing_packets;
use crate::{
Client,
attack::handle_attack_event,
inventory::{Inventory, InventorySet},
local_player::{LocalGameMode, PermissionLevel, PlayerAbilities},
movement::MoveEventsSet,
- packet_handling::game::{SendPacketEvent, handle_send_packet_event},
+ packet::game::SendPacketEvent,
respawn::perform_respawn,
};
@@ -54,7 +55,7 @@ impl Plugin for InteractPlugin {
handle_block_interact_event,
handle_swing_arm_event,
)
- .before(handle_send_packet_event)
+ .before(handle_outgoing_packets)
.after(InventorySet)
.after(perform_respawn)
.after(handle_attack_event)
diff --git a/azalea-client/src/inventory.rs b/azalea-client/src/plugins/inventory.rs
index 4d796c9c..3f823ca2 100644
--- a/azalea-client/src/inventory.rs
+++ b/azalea-client/src/plugins/inventory.rs
@@ -25,11 +25,9 @@ use bevy_ecs::{
};
use tracing::warn;
+use super::packet::game::handle_outgoing_packets;
use crate::{
- Client,
- local_player::PlayerAbilities,
- packet_handling::game::{SendPacketEvent, handle_send_packet_event},
- respawn::perform_respawn,
+ Client, local_player::PlayerAbilities, packet::game::SendPacketEvent, respawn::perform_respawn,
};
pub struct InventoryPlugin;
@@ -48,7 +46,7 @@ impl Plugin for InventoryPlugin {
handle_menu_opened_event,
handle_set_container_content_event,
handle_container_click_event,
- handle_container_close_event.before(handle_send_packet_event),
+ handle_container_close_event.before(handle_outgoing_packets),
handle_client_side_close_container_event,
)
.chain()
diff --git a/azalea-client/src/mining.rs b/azalea-client/src/plugins/mining.rs
index 03063b3e..beb380b7 100644
--- a/azalea-client/src/mining.rs
+++ b/azalea-client/src/plugins/mining.rs
@@ -18,12 +18,12 @@ use crate::{
inventory::{Inventory, InventorySet},
local_player::{LocalGameMode, PermissionLevel, PlayerAbilities},
movement::MoveEventsSet,
- packet_handling::game::SendPacketEvent,
+ packet::game::SendPacketEvent,
};
/// A plugin that allows clients to break blocks in the world.
-pub struct MinePlugin;
-impl Plugin for MinePlugin {
+pub struct MiningPlugin;
+impl Plugin for MiningPlugin {
fn build(&self, app: &mut App) {
app.add_event::<StartMiningBlockEvent>()
.add_event::<StartMiningBlockWithDirectionEvent>()
@@ -59,6 +59,7 @@ impl Plugin for MinePlugin {
}
}
+/// The Bevy system set for things related to mining.
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub struct MiningSet;
diff --git a/azalea-client/src/plugins/mod.rs b/azalea-client/src/plugins/mod.rs
new file mode 100644
index 00000000..11794fb3
--- /dev/null
+++ b/azalea-client/src/plugins/mod.rs
@@ -0,0 +1,14 @@
+pub mod attack;
+pub mod brand;
+pub mod chat;
+pub mod chunks;
+pub mod disconnect;
+pub mod events;
+pub mod interact;
+pub mod inventory;
+pub mod mining;
+pub mod movement;
+pub mod packet;
+pub mod respawn;
+pub mod task_pool;
+pub mod tick_end;
diff --git a/azalea-client/src/movement.rs b/azalea-client/src/plugins/movement.rs
index b0ff70f4..17b92e65 100644
--- a/azalea-client/src/movement.rs
+++ b/azalea-client/src/plugins/movement.rs
@@ -27,7 +27,7 @@ use bevy_ecs::{
use thiserror::Error;
use crate::client::Client;
-use crate::packet_handling::game::SendPacketEvent;
+use crate::packet::game::SendPacketEvent;
#[derive(Error, Debug)]
pub enum MovePlayerError {
@@ -47,9 +47,9 @@ impl From<MoveEntityError> for MovePlayerError {
}
}
-pub struct PlayerMovePlugin;
+pub struct MovementPlugin;
-impl Plugin for PlayerMovePlugin {
+impl Plugin for MovementPlugin {
fn build(&self, app: &mut App) {
app.add_event::<StartWalkEvent>()
.add_event::<StartSprintEvent>()
diff --git a/azalea-client/src/plugins/packet/config/events.rs b/azalea-client/src/plugins/packet/config/events.rs
new file mode 100644
index 00000000..6b647d74
--- /dev/null
+++ b/azalea-client/src/plugins/packet/config/events.rs
@@ -0,0 +1,90 @@
+use std::io::Cursor;
+
+use azalea_protocol::{
+ packets::{
+ config::{ClientboundConfigPacket, ServerboundConfigPacket},
+ Packet,
+ },
+ read::deserialize_packet,
+};
+use bevy_ecs::prelude::*;
+use tracing::{debug, error};
+
+use crate::{raw_connection::RawConnection, InConfigState};
+
+#[derive(Event, Debug, Clone)]
+pub struct ReceiveConfigPacketEvent {
+ /// The client entity that received the packet.
+ pub entity: Entity,
+ /// The packet that was actually received.
+ pub packet: ClientboundConfigPacket,
+}
+
+/// An event for sending a packet to the server while we're in the
+/// `configuration` state.
+#[derive(Event)]
+pub struct SendConfigPacketEvent {
+ pub sent_by: Entity,
+ pub packet: ServerboundConfigPacket,
+}
+impl SendConfigPacketEvent {
+ pub fn new(sent_by: Entity, packet: impl Packet<ServerboundConfigPacket>) -> Self {
+ let packet = packet.into_variant();
+ Self { sent_by, packet }
+ }
+}
+
+pub fn handle_send_packet_event(
+ mut send_packet_events: EventReader<SendConfigPacketEvent>,
+ mut query: Query<(&mut RawConnection, Option<&InConfigState>)>,
+) {
+ for event in send_packet_events.read() {
+ if let Ok((raw_conn, in_configuration_state)) = query.get_mut(event.sent_by) {
+ if in_configuration_state.is_none() {
+ error!(
+ "Tried to send a configuration packet {:?} while not in configuration state",
+ event.packet
+ );
+ continue;
+ }
+ debug!("Sending packet: {:?}", event.packet);
+ if let Err(e) = raw_conn.write_packet(event.packet.clone()) {
+ error!("Failed to send packet: {e}");
+ }
+ }
+ }
+}
+
+pub fn send_packet_events(
+ query: Query<(Entity, &RawConnection), With<InConfigState>>,
+ mut packet_events: ResMut<Events<ReceiveConfigPacketEvent>>,
+) {
+ // we manually clear and send the events at the beginning of each update
+ // since otherwise it'd cause issues with events in process_packet_events
+ // running twice
+ packet_events.clear();
+ for (player_entity, raw_conn) in &query {
+ let packets_lock = raw_conn.incoming_packet_queue();
+ let mut packets = packets_lock.lock();
+ if !packets.is_empty() {
+ for raw_packet in packets.iter() {
+ let packet = match deserialize_packet::<ClientboundConfigPacket>(&mut Cursor::new(
+ raw_packet,
+ )) {
+ Ok(packet) => packet,
+ Err(err) => {
+ error!("failed to read packet: {err:?}");
+ debug!("packet bytes: {raw_packet:?}");
+ continue;
+ }
+ };
+ packet_events.send(ReceiveConfigPacketEvent {
+ entity: player_entity,
+ packet,
+ });
+ }
+ // clear the packets right after we read them
+ packets.clear();
+ }
+ }
+}
diff --git a/azalea-client/src/plugins/packet/config/mod.rs b/azalea-client/src/plugins/packet/config/mod.rs
new file mode 100644
index 00000000..5cb19b9d
--- /dev/null
+++ b/azalea-client/src/plugins/packet/config/mod.rs
@@ -0,0 +1,223 @@
+mod events;
+
+use azalea_protocol::packets::config::*;
+use azalea_protocol::packets::ConnectionProtocol;
+use bevy_ecs::prelude::*;
+use bevy_ecs::system::SystemState;
+pub use events::*;
+use tracing::{debug, warn};
+
+use super::as_system;
+use crate::client::InConfigState;
+use crate::disconnect::DisconnectEvent;
+use crate::packet::game::KeepAliveEvent;
+use crate::raw_connection::RawConnection;
+use crate::{declare_packet_handlers, InstanceHolder};
+
+pub fn process_packet_events(ecs: &mut World) {
+ let mut events_owned = Vec::new();
+ let mut system_state: SystemState<EventReader<ReceiveConfigPacketEvent>> =
+ SystemState::new(ecs);
+ let mut events = system_state.get_mut(ecs);
+ for ReceiveConfigPacketEvent {
+ entity: player_entity,
+ packet,
+ } in events.read()
+ {
+ // we do this so `ecs` isn't borrowed for the whole loop
+ events_owned.push((*player_entity, packet.clone()));
+ }
+ for (player_entity, packet) in events_owned {
+ let mut handler = ConfigPacketHandler {
+ player: player_entity,
+ ecs,
+ };
+
+ declare_packet_handlers!(
+ ClientboundConfigPacket,
+ packet,
+ handler,
+ [
+ cookie_request,
+ custom_payload,
+ disconnect,
+ finish_configuration,
+ keep_alive,
+ ping,
+ reset_chat,
+ registry_data,
+ resource_pack_pop,
+ resource_pack_push,
+ store_cookie,
+ transfer,
+ update_enabled_features,
+ update_tags,
+ select_known_packs,
+ custom_report_details,
+ server_links,
+ ]
+ );
+ }
+}
+
+pub struct ConfigPacketHandler<'a> {
+ pub ecs: &'a mut World,
+ pub player: Entity,
+}
+impl ConfigPacketHandler<'_> {
+ pub fn registry_data(&mut self, p: ClientboundRegistryData) {
+ as_system::<Query<&mut InstanceHolder>>(self.ecs, |mut query| {
+ let instance_holder = query.get_mut(self.player).unwrap();
+ let mut instance = instance_holder.instance.write();
+
+ // add the new registry data
+ instance.registries.append(p.registry_id, p.entries);
+ });
+ }
+
+ pub fn custom_payload(&mut self, p: ClientboundCustomPayload) {
+ debug!("Got custom payload packet {p:?}");
+ }
+
+ pub fn disconnect(&mut self, p: ClientboundDisconnect) {
+ warn!("Got disconnect packet {p:?}");
+ as_system::<EventWriter<_>>(self.ecs, |mut events| {
+ events.send(DisconnectEvent {
+ entity: self.player,
+ reason: Some(p.reason),
+ });
+ });
+ }
+
+ pub fn finish_configuration(&mut self, p: ClientboundFinishConfiguration) {
+ debug!("got FinishConfiguration packet: {p:?}");
+
+ as_system::<(Commands, Query<&mut RawConnection>)>(
+ self.ecs,
+ |(mut commands, mut query)| {
+ let mut raw_conn = query.get_mut(self.player).unwrap();
+
+ raw_conn
+ .write_packet(ServerboundFinishConfiguration)
+ .expect(
+ "we should be in the right state and encoding this packet shouldn't fail",
+ );
+ raw_conn.set_state(ConnectionProtocol::Game);
+
+ // these components are added now that we're going to be in the Game state
+ commands
+ .entity(self.player)
+ .remove::<InConfigState>()
+ .insert(crate::JoinedClientBundle::default());
+ },
+ );
+ }
+
+ pub fn keep_alive(&mut self, p: ClientboundKeepAlive) {
+ debug!(
+ "Got keep alive packet (in configuration) {p:?} for {:?}",
+ self.player
+ );
+
+ as_system::<(Query<&RawConnection>, EventWriter<_>)>(self.ecs, |(query, mut events)| {
+ let raw_conn = query.get(self.player).unwrap();
+
+ events.send(KeepAliveEvent {
+ entity: self.player,
+ id: p.id,
+ });
+ raw_conn
+ .write_packet(ServerboundKeepAlive { id: p.id })
+ .unwrap();
+ });
+ }
+
+ pub fn ping(&mut self, p: ClientboundPing) {
+ debug!("Got ping packet (in configuration) {p:?}");
+
+ as_system::<Query<&RawConnection>>(self.ecs, |query| {
+ let raw_conn = query.get(self.player).unwrap();
+
+ raw_conn.write_packet(ServerboundPong { id: p.id }).unwrap();
+ });
+ }
+
+ pub fn resource_pack_push(&mut self, p: ClientboundResourcePackPush) {
+ debug!("Got resource pack push packet {p:?}");
+
+ as_system::<Query<&RawConnection>>(self.ecs, |query| {
+ let raw_conn = query.get(self.player).unwrap();
+
+ // always accept resource pack
+ raw_conn
+ .write_packet(ServerboundResourcePack {
+ id: p.id,
+ action: s_resource_pack::Action::Accepted,
+ })
+ .unwrap();
+ });
+ }
+
+ pub fn resource_pack_pop(&mut self, p: ClientboundResourcePackPop) {
+ debug!("Got resource pack pop packet {p:?}");
+ }
+
+ pub fn update_enabled_features(&mut self, p: ClientboundUpdateEnabledFeatures) {
+ debug!("Got update enabled features packet {p:?}");
+ }
+
+ pub fn update_tags(&mut self, _p: ClientboundUpdateTags) {
+ debug!("Got update tags packet");
+ }
+
+ pub fn cookie_request(&mut self, p: ClientboundCookieRequest) {
+ debug!("Got cookie request packet {p:?}");
+
+ as_system::<Query<&RawConnection>>(self.ecs, |query| {
+ let raw_conn = query.get(self.player).unwrap();
+
+ raw_conn
+ .write_packet(ServerboundCookieResponse {
+ key: p.key,
+ // cookies aren't implemented
+ payload: None,
+ })
+ .unwrap();
+ });
+ }
+
+ pub fn reset_chat(&mut self, p: ClientboundResetChat) {
+ debug!("Got reset chat packet {p:?}");
+ }
+
+ pub fn store_cookie(&mut self, p: ClientboundStoreCookie) {
+ debug!("Got store cookie packet {p:?}");
+ }
+
+ pub fn transfer(&mut self, p: ClientboundTransfer) {
+ debug!("Got transfer packet {p:?}");
+ }
+
+ pub fn select_known_packs(&mut self, p: ClientboundSelectKnownPacks) {
+ debug!("Got select known packs packet {p:?}");
+
+ as_system::<Query<&RawConnection>>(self.ecs, |query| {
+ let raw_conn = query.get(self.player).unwrap();
+
+ // resource pack management isn't implemented
+ raw_conn
+ .write_packet(ServerboundSelectKnownPacks {
+ known_packs: vec![],
+ })
+ .unwrap();
+ });
+ }
+
+ pub fn server_links(&mut self, p: ClientboundServerLinks) {
+ debug!("Got server links packet {p:?}");
+ }
+
+ pub fn custom_report_details(&mut self, p: ClientboundCustomReportDetails) {
+ debug!("Got custom report details packet {p:?}");
+ }
+}
diff --git a/azalea-client/src/plugins/packet/game/events.rs b/azalea-client/src/plugins/packet/game/events.rs
new file mode 100644
index 00000000..19f2a571
--- /dev/null
+++ b/azalea-client/src/plugins/packet/game/events.rs
@@ -0,0 +1,178 @@
+use std::{
+ io::Cursor,
+ sync::{Arc, Weak},
+};
+
+use azalea_chat::FormattedText;
+use azalea_core::resource_location::ResourceLocation;
+use azalea_entity::LocalEntity;
+use azalea_protocol::{
+ packets::{
+ Packet,
+ game::{ClientboundGamePacket, ClientboundPlayerCombatKill, ServerboundGamePacket},
+ },
+ read::deserialize_packet,
+};
+use azalea_world::Instance;
+use bevy_ecs::prelude::*;
+use parking_lot::RwLock;
+use tracing::{debug, error};
+use uuid::Uuid;
+
+use crate::{PlayerInfo, raw_connection::RawConnection};
+
+/// An event that's sent when we receive a packet.
+/// ```
+/// # use azalea_client::packet::game::ReceivePacketEvent;
+/// # use azalea_protocol::packets::game::ClientboundGamePacket;
+/// # use bevy_ecs::event::EventReader;
+///
+/// fn handle_packets(mut events: EventReader<ReceivePacketEvent>) {
+/// for ReceivePacketEvent {
+/// entity,
+/// packet,
+/// } in events.read() {
+/// match packet.as_ref() {
+/// ClientboundGamePacket::LevelParticles(p) => {
+/// // ...
+/// }
+/// _ => {}
+/// }
+/// }
+/// }
+/// ```
+#[derive(Event, Debug, Clone)]
+pub struct ReceivePacketEvent {
+ /// The client entity that received the packet.
+ pub entity: Entity,
+ /// The packet that was actually received.
+ pub packet: Arc<ClientboundGamePacket>,
+}
+
+/// An event for sending a packet to the server while we're in the `game` state.
+#[derive(Event)]
+pub struct SendPacketEvent {
+ pub sent_by: Entity,
+ pub packet: ServerboundGamePacket,
+}
+impl SendPacketEvent {
+ pub fn new(sent_by: Entity, packet: impl Packet<ServerboundGamePacket>) -> Self {
+ let packet = packet.into_variant();
+ Self { sent_by, packet }
+ }
+}
+
+pub fn handle_outgoing_packets(
+ mut send_packet_events: EventReader<SendPacketEvent>,
+ mut query: Query<&mut RawConnection>,
+) {
+ for event in send_packet_events.read() {
+ if let Ok(raw_connection) = query.get_mut(event.sent_by) {
+ // debug!("Sending packet: {:?}", event.packet);
+ if let Err(e) = raw_connection.write_packet(event.packet.clone()) {
+ error!("Failed to send packet: {e}");
+ }
+ }
+ }
+}
+
+pub fn send_receivepacketevent(
+ query: Query<(Entity, &RawConnection), With<LocalEntity>>,
+ mut packet_events: ResMut<Events<ReceivePacketEvent>>,
+) {
+ // we manually clear and send the events at the beginning of each update
+ // since otherwise it'd cause issues with events in process_packet_events
+ // running twice
+ packet_events.clear();
+ for (player_entity, raw_connection) in &query {
+ let packets_lock = raw_connection.incoming_packet_queue();
+ let mut packets = packets_lock.lock();
+ if !packets.is_empty() {
+ for raw_packet in packets.iter() {
+ let packet =
+ match deserialize_packet::<ClientboundGamePacket>(&mut Cursor::new(raw_packet))
+ {
+ Ok(packet) => packet,
+ Err(err) => {
+ error!("failed to read packet: {err:?}");
+ debug!("packet bytes: {raw_packet:?}");
+ continue;
+ }
+ };
+ packet_events.send(ReceivePacketEvent {
+ entity: player_entity,
+ packet: Arc::new(packet),
+ });
+ }
+ // clear the packets right after we read them
+ packets.clear();
+ }
+ }
+}
+
+/// A player joined the game (or more specifically, was added to the tab
+/// list of a local player).
+#[derive(Event, Debug, Clone)]
+pub struct AddPlayerEvent {
+ /// The local player entity that received this event.
+ pub entity: Entity,
+ pub info: PlayerInfo,
+}
+/// A player left the game (or maybe is still in the game and was just
+/// removed from the tab list of a local player).
+#[derive(Event, Debug, Clone)]
+pub struct RemovePlayerEvent {
+ /// The local player entity that received this event.
+ pub entity: Entity,
+ pub info: PlayerInfo,
+}
+/// A player was updated in the tab list of a local player (gamemode, display
+/// name, or latency changed).
+#[derive(Event, Debug, Clone)]
+pub struct UpdatePlayerEvent {
+ /// The local player entity that received this event.
+ pub entity: Entity,
+ pub info: PlayerInfo,
+}
+
+/// Event for when an entity dies. dies. If it's a local player and there's a
+/// reason in the death screen, the [`ClientboundPlayerCombatKill`] will
+/// be included.
+#[derive(Event, Debug, Clone)]
+pub struct DeathEvent {
+ pub entity: Entity,
+ pub packet: Option<ClientboundPlayerCombatKill>,
+}
+
+/// A KeepAlive packet is sent from the server to verify that the client is
+/// still connected.
+#[derive(Event, Debug, Clone)]
+pub struct KeepAliveEvent {
+ pub entity: Entity,
+ /// The ID of the keepalive. This is an arbitrary number, but vanilla
+ /// servers use the time to generate this.
+ pub id: u64,
+}
+
+#[derive(Event, Debug, Clone)]
+pub struct ResourcePackEvent {
+ pub entity: Entity,
+ /// The random ID for this request to download the resource pack. The packet
+ /// for replying to a resource pack push must contain the same ID.
+ pub id: Uuid,
+ pub url: String,
+ pub hash: String,
+ pub required: bool,
+ pub prompt: Option<FormattedText>,
+}
+
+/// An instance (aka world, dimension) was loaded by a client.
+///
+/// Since the instance is given to you as a weak reference, it won't be able to
+/// be `upgrade`d if all local players leave it.
+#[derive(Event, Debug, Clone)]
+pub struct InstanceLoadedEvent {
+ pub entity: Entity,
+ pub name: ResourceLocation,
+ pub instance: Weak<RwLock<Instance>>,
+}
diff --git a/azalea-client/src/plugins/packet/game/mod.rs b/azalea-client/src/plugins/packet/game/mod.rs
new file mode 100644
index 00000000..98f76d13
--- /dev/null
+++ b/azalea-client/src/plugins/packet/game/mod.rs
@@ -0,0 +1,1583 @@
+mod events;
+
+use std::{collections::HashSet, ops::Add, sync::Arc};
+
+use azalea_core::{
+ game_type::GameMode,
+ math,
+ position::{ChunkPos, Vec3},
+};
+use azalea_entity::{
+ Dead, EntityBundle, EntityKind, LastSentPosition, LoadedBy, LocalEntity, LookDirection,
+ Physics, Position, RelativeEntityUpdate,
+ indexing::{EntityIdIndex, EntityUuidIndex},
+ metadata::{Health, apply_metadata},
+};
+use azalea_protocol::packets::game::*;
+use azalea_world::{InstanceContainer, InstanceName, MinecraftEntityId, PartialInstance};
+use bevy_ecs::{prelude::*, system::SystemState};
+pub use events::*;
+use tracing::{debug, error, trace, warn};
+
+use crate::{
+ ClientInformation, PlayerInfo,
+ chat::{ChatPacket, ChatReceivedEvent},
+ chunks, declare_packet_handlers,
+ disconnect::DisconnectEvent,
+ inventory::{
+ ClientSideCloseContainerEvent, Inventory, MenuOpenedEvent, SetContainerContentEvent,
+ },
+ local_player::{
+ GameProfileComponent, Hunger, InstanceHolder, LocalGameMode, PlayerAbilities, TabList,
+ },
+ movement::{KnockbackEvent, KnockbackType},
+ packet::as_system,
+};
+
+pub fn process_packet_events(ecs: &mut World) {
+ let mut events_owned = Vec::<(Entity, Arc<ClientboundGamePacket>)>::new();
+
+ {
+ let mut system_state = SystemState::<EventReader<ReceivePacketEvent>>::new(ecs);
+ let mut events = system_state.get_mut(ecs);
+ for ReceivePacketEvent {
+ entity: player_entity,
+ packet,
+ } in events.read()
+ {
+ // we do this so `ecs` isn't borrowed for the whole loop
+ events_owned.push((*player_entity, packet.clone()));
+ }
+ }
+
+ for (player_entity, packet) in events_owned {
+ let mut handler = GamePacketHandler {
+ player: player_entity,
+ ecs,
+ };
+
+ declare_packet_handlers!(
+ ClientboundGamePacket,
+ packet.as_ref(),
+ handler,
+ [
+ login,
+ set_chunk_cache_radius,
+ chunk_batch_start,
+ chunk_batch_finished,
+ custom_payload,
+ change_difficulty,
+ commands,
+ player_abilities,
+ set_cursor_item,
+ update_tags,
+ disconnect,
+ update_recipes,
+ entity_event,
+ player_position,
+ player_info_update,
+ player_info_remove,
+ set_chunk_cache_center,
+ chunks_biomes,
+ light_update,
+ level_chunk_with_light,
+ add_entity,
+ set_entity_data,
+ update_attributes,
+ set_entity_motion,
+ set_entity_link,
+ initialize_border,
+ set_time,
+ set_default_spawn_position,
+ set_health,
+ set_experience,
+ teleport_entity,
+ update_advancements,
+ rotate_head,
+ move_entity_pos,
+ move_entity_pos_rot,
+ move_entity_rot,
+ keep_alive,
+ remove_entities,
+ player_chat,
+ system_chat,
+ disguised_chat,
+ sound,
+ level_event,
+ block_update,
+ animate,
+ section_blocks_update,
+ game_event,
+ level_particles,
+ server_data,
+ set_equipment,
+ update_mob_effect,
+ add_experience_orb,
+ award_stats,
+ block_changed_ack,
+ block_destruction,
+ block_entity_data,
+ block_event,
+ boss_event,
+ command_suggestions,
+ container_set_content,
+ container_set_data,
+ container_set_slot,
+ container_close,
+ cooldown,
+ custom_chat_completions,
+ delete_chat,
+ explode,
+ forget_level_chunk,
+ horse_screen_open,
+ map_item_data,
+ merchant_offers,
+ move_vehicle,
+ open_book,
+ open_screen,
+ open_sign_editor,
+ ping,
+ place_ghost_recipe,
+ player_combat_end,
+ player_combat_enter,
+ player_combat_kill,
+ player_look_at,
+ remove_mob_effect,
+ resource_pack_push,
+ resource_pack_pop,
+ respawn,
+ start_configuration,
+ entity_position_sync,
+ select_advancements_tab,
+ set_action_bar_text,
+ set_border_center,
+ set_border_lerp_size,
+ set_border_size,
+ set_border_warning_delay,
+ set_border_warning_distance,
+ set_camera,
+ set_display_objective,
+ set_objective,
+ set_passengers,
+ set_player_team,
+ set_score,
+ set_simulation_distance,
+ set_subtitle_text,
+ set_title_text,
+ set_titles_animation,
+ clear_titles,
+ sound_entity,
+ stop_sound,
+ tab_list,
+ tag_query,
+ take_item_entity,
+ bundle_delimiter,
+ damage_event,
+ hurt_animation,
+ ticking_state,
+ ticking_step,
+ reset_score,
+ cookie_request,
+ debug_sample,
+ pong_response,
+ store_cookie,
+ transfer,
+ move_minecart_along_track,
+ set_held_slot,
+ set_player_inventory,
+ projectile_power,
+ custom_report_details,
+ server_links,
+ player_rotation,
+ recipe_book_add,
+ recipe_book_remove,
+ recipe_book_settings,
+ ]
+ );
+ }
+}
+
+pub struct GamePacketHandler<'a> {
+ pub ecs: &'a mut World,
+ pub player: Entity,
+}
+impl GamePacketHandler<'_> {
+ pub fn login(&mut self, p: &ClientboundLogin) {
+ debug!("Got login packet");
+
+ as_system::<(
+ Commands,
+ Query<(
+ &GameProfileComponent,
+ &ClientInformation,
+ Option<&mut InstanceName>,
+ Option<&mut LoadedBy>,
+ &mut EntityIdIndex,
+ &mut InstanceHolder,
+ )>,
+ EventWriter<InstanceLoadedEvent>,
+ ResMut<InstanceContainer>,
+ ResMut<EntityUuidIndex>,
+ EventWriter<SendPacketEvent>,
+ )>(
+ self.ecs,
+ |(
+ mut commands,
+ mut query,
+ mut instance_loaded_events,
+ mut instance_container,
+ mut entity_uuid_index,
+ mut send_packet_events,
+ )| {
+ let (
+ game_profile,
+ client_information,
+ instance_name,
+ loaded_by,
+ mut entity_id_index,
+ mut instance_holder,
+ ) = query.get_mut(self.player).unwrap();
+
+ let new_instance_name = p.common.dimension.clone();
+
+ if let Some(mut instance_name) = instance_name {
+ *instance_name = instance_name.clone();
+ } else {
+ commands
+ .entity(self.player)
+ .insert(InstanceName(new_instance_name.clone()));
+ }
+
+ let Some((_dimension_type, dimension_data)) = p
+ .common
+ .dimension_type(&instance_holder.instance.read().registries)
+ else {
+ return;
+ };
+
+ // add this world to the instance_container (or don't if it's already
+ // there)
+ let weak_instance = instance_container.insert(
+ new_instance_name.clone(),
+ dimension_data.height,
+ dimension_data.min_y,
+ &instance_holder.instance.read().registries,
+ );
+ instance_loaded_events.send(InstanceLoadedEvent {
+ entity: self.player,
+ name: new_instance_name.clone(),
+ instance: Arc::downgrade(&weak_instance),
+ });
+
+ // set the partial_world to an empty world
+ // (when we add chunks or entities those will be in the
+ // instance_container)
+
+ *instance_holder.partial_instance.write() = PartialInstance::new(
+ azalea_world::chunk_storage::calculate_chunk_storage_range(
+ client_information.view_distance.into(),
+ ),
+ // this argument makes it so other clients don't update this player entity
+ // in a shared instance
+ Some(self.player),
+ );
+ {
+ let map = instance_holder.instance.read().registries.map.clone();
+ let new_registries = &mut weak_instance.write().registries;
+ // add the registries from this instance to the weak instance
+ for (registry_name, registry) in map {
+ new_registries.map.insert(registry_name, registry);
+ }
+ }
+ instance_holder.instance = weak_instance;
+
+ let entity_bundle = EntityBundle::new(
+ game_profile.uuid,
+ Vec3::default(),
+ azalea_registry::EntityKind::Player,
+ new_instance_name,
+ );
+ let entity_id = p.player_id;
+ // insert our components into the ecs :)
+ commands.entity(self.player).insert((
+ entity_id,
+ LocalGameMode {
+ current: p.common.game_type,
+ previous: p.common.previous_game_type.into(),
+ },
+ entity_bundle,
+ ));
+
+ azalea_entity::indexing::add_entity_to_indexes(
+ entity_id,
+ self.player,
+ Some(game_profile.uuid),
+ &mut entity_id_index,
+ &mut entity_uuid_index,
+ &mut instance_holder.instance.write(),
+ );
+
+ // update or insert loaded_by
+ if let Some(mut loaded_by) = loaded_by {
+ loaded_by.insert(self.player);
+ } else {
+ commands
+ .entity(self.player)
+ .insert(LoadedBy(HashSet::from_iter(vec![self.player])));
+ }
+
+ // send the client information that we have set
+ debug!(
+ "Sending client information because login: {:?}",
+ client_information
+ );
+ send_packet_events.send(SendPacketEvent::new(self.player,
+ azalea_protocol::packets::game::s_client_information::ServerboundClientInformation { information: client_information.clone() },
+ ));
+ },
+ );
+ }
+
+ pub fn set_chunk_cache_radius(&mut self, p: &ClientboundSetChunkCacheRadius) {
+ debug!("Got set chunk cache radius packet {p:?}");
+ }
+
+ pub fn chunk_batch_start(&mut self, _p: &ClientboundChunkBatchStart) {
+ // the packet is empty, it's just a marker to tell us when the batch starts and
+ // ends
+ debug!("Got chunk batch start");
+
+ as_system::<EventWriter<_>>(self.ecs, |mut events| {
+ events.send(chunks::ChunkBatchStartEvent {
+ entity: self.player,
+ });
+ });
+ }
+
+ pub fn chunk_batch_finished(&mut self, p: &ClientboundChunkBatchFinished) {
+ debug!("Got chunk batch finished {p:?}");
+
+ as_system::<EventWriter<_>>(self.ecs, |mut events| {
+ events.send(chunks::ChunkBatchFinishedEvent {
+ entity: self.player,
+ batch_size: p.batch_size,
+ });
+ });
+ }
+
+ pub fn custom_payload(&mut self, p: &ClientboundCustomPayload) {
+ debug!("Got custom payload packet {p:?}");
+ }
+
+ pub fn change_difficulty(&mut self, p: &ClientboundChangeDifficulty) {
+ debug!("Got difficulty packet {p:?}");
+ }
+
+ pub fn commands(&mut self, _p: &ClientboundCommands) {
+ debug!("Got declare commands packet");
+ }
+
+ pub fn player_abilities(&mut self, p: &ClientboundPlayerAbilities) {
+ debug!("Got player abilities packet {p:?}");
+
+ as_system::<Query<&mut PlayerAbilities>>(self.ecs, |mut query| {
+ let mut player_abilities = query.get_mut(self.player).unwrap();
+
+ *player_abilities = PlayerAbilities::from(p);
+ });
+ }
+
+ pub fn set_cursor_item(&mut self, p: &ClientboundSetCursorItem) {
+ debug!("Got set cursor item packet {p:?}");
+ }
+
+ pub fn update_tags(&mut self, _p: &ClientboundUpdateTags) {
+ debug!("Got update tags packet");
+ }
+
+ pub fn disconnect(&mut self, p: &ClientboundDisconnect) {
+ warn!("Got disconnect packet {p:?}");
+
+ as_system::<EventWriter<_>>(self.ecs, |mut events| {
+ events.send(DisconnectEvent {
+ entity: self.player,
+ reason: Some(p.reason.clone()),
+ });
+ });
+ }
+
+ pub fn update_recipes(&mut self, _p: &ClientboundUpdateRecipes) {
+ debug!("Got update recipes packet");
+ }
+
+ pub fn entity_event(&mut self, _p: &ClientboundEntityEvent) {
+ // debug!("Got entity event packet {p:?}");
+ }
+
+ pub fn player_position(&mut self, p: &ClientboundPlayerPosition) {
+ debug!("Got player position packet {p:?}");
+
+ as_system::<(
+ Query<(
+ &mut Physics,
+ &mut LookDirection,
+ &mut Position,
+ &mut LastSentPosition,
+ )>,
+ EventWriter<SendPacketEvent>,
+ )>(self.ecs, |(mut query, mut send_packet_events)| {
+ let Ok((mut physics, mut direction, mut position, mut last_sent_position)) =
+ query.get_mut(self.player)
+ else {
+ return;
+ };
+
+ **last_sent_position = **position;
+
+ fn apply_change<T: Add<Output = T>>(base: T, condition: bool, change: T) -> T {
+ if condition { base + change } else { change }
+ }
+
+ let new_x = apply_change(position.x, p.relative.x, p.change.pos.x);
+ let new_y = apply_change(position.y, p.relative.y, p.change.pos.y);
+ let new_z = apply_change(position.z, p.relative.z, p.change.pos.z);
+
+ let new_y_rot = apply_change(
+ direction.y_rot,
+ p.relative.y_rot,
+ p.change.look_direction.y_rot,
+ );
+ let new_x_rot = apply_change(
+ direction.x_rot,
+ p.relative.x_rot,
+ p.change.look_direction.x_rot,
+ );
+
+ let mut new_delta_from_rotations = physics.velocity;
+ if p.relative.rotate_delta {
+ let y_rot_delta = direction.y_rot - new_y_rot;
+ let x_rot_delta = direction.x_rot - new_x_rot;
+ new_delta_from_rotations = new_delta_from_rotations
+ .x_rot(math::to_radians(x_rot_delta as f64) as f32)
+ .y_rot(math::to_radians(y_rot_delta as f64) as f32);
+ }
+
+ let new_delta = Vec3::new(
+ apply_change(
+ new_delta_from_rotations.x,
+ p.relative.delta_x,
+ p.change.delta.x,
+ ),
+ apply_change(
+ new_delta_from_rotations.y,
+ p.relative.delta_y,
+ p.change.delta.y,
+ ),
+ apply_change(
+ new_delta_from_rotations.z,
+ p.relative.delta_z,
+ p.change.delta.z,
+ ),
+ );
+
+ // apply the updates
+
+ physics.velocity = new_delta;
+
+ (direction.y_rot, direction.x_rot) = (new_y_rot, new_x_rot);
+
+ let new_pos = Vec3::new(new_x, new_y, new_z);
+ if new_pos != **position {
+ **position = new_pos;
+ }
+
+ // old_pos is set to the current position when we're teleported
+ physics.set_old_pos(&position);
+
+ // send the relevant packets
+
+ send_packet_events.send(SendPacketEvent::new(
+ self.player,
+ ServerboundAcceptTeleportation { id: p.id },
+ ));
+ send_packet_events.send(SendPacketEvent::new(
+ self.player,
+ ServerboundMovePlayerPosRot {
+ pos: new_pos,
+ look_direction: LookDirection::new(new_y_rot, new_x_rot),
+ // this is always false
+ on_ground: false,
+ },
+ ));
+ });
+ }
+
+ pub fn player_info_update(&mut self, p: &ClientboundPlayerInfoUpdate) {
+ debug!("Got player info packet {p:?}");
+
+ as_system::<(
+ Query<&mut TabList>,
+ EventWriter<AddPlayerEvent>,
+ EventWriter<UpdatePlayerEvent>,
+ ResMut<TabList>,
+ )>(
+ self.ecs,
+ |(
+ mut query,
+ mut add_player_events,
+ mut update_player_events,
+ mut tab_list_resource,
+ )| {
+ let mut tab_list = query.get_mut(self.player).unwrap();
+
+ for updated_info in &p.entries {
+ // add the new player maybe
+ if p.actions.add_player {
+ let info = PlayerInfo {
+ profile: updated_info.profile.clone(),
+ uuid: updated_info.profile.uuid,
+ gamemode: updated_info.game_mode,
+ latency: updated_info.latency,
+ display_name: updated_info.display_name.clone(),
+ };
+ tab_list.insert(updated_info.profile.uuid, info.clone());
+ add_player_events.send(AddPlayerEvent {
+ entity: self.player,
+ info: info.clone(),
+ });
+ } else if let Some(info) = tab_list.get_mut(&updated_info.profile.uuid) {
+ // `else if` because the block for add_player above
+ // already sets all the fields
+ if p.actions.update_game_mode {
+ info.gamemode = updated_info.game_mode;
+ }
+ if p.actions.update_latency {
+ info.latency = updated_info.latency;
+ }
+ if p.actions.update_display_name {
+ info.display_name.clone_from(&updated_info.display_name);
+ }
+ update_player_events.send(UpdatePlayerEvent {
+ entity: self.player,
+ info: info.clone(),
+ });
+ } else {
+ let uuid = updated_info.profile.uuid;
+ debug!("Ignoring PlayerInfoUpdate for unknown player {uuid}");
+ }
+ }
+
+ *tab_list_resource = tab_list.clone();
+ },
+ );
+ }
+
+ pub fn player_info_remove(&mut self, p: &ClientboundPlayerInfoRemove) {
+ debug!("Got chunk cache center packet {p:?}");
+
+ as_system::<(
+ Query<&mut TabList>,
+ EventWriter<RemovePlayerEvent>,
+ ResMut<TabList>,
+ )>(
+ self.ecs,
+ |(mut query, mut remove_player_events, mut tab_list_resource)| {
+ let mut tab_list = query.get_mut(self.player).unwrap();
+
+ for uuid in &p.profile_ids {
+ if let Some(info) = tab_list.remove(uuid) {
+ remove_player_events.send(RemovePlayerEvent {
+ entity: self.player,
+ info,
+ });
+ }
+ tab_list_resource.remove(uuid);
+ }
+ },
+ );
+ }
+
+ pub fn set_chunk_cache_center(&mut self, p: &ClientboundSetChunkCacheCenter) {
+ debug!("Got chunk cache center packet {p:?}");
+
+ as_system::<Query<&mut InstanceHolder>>(self.ecs, |mut query| {
+ let instance_holder = query.get_mut(self.player).unwrap();
+ let mut partial_world = instance_holder.partial_instance.write();
+
+ partial_world
+ .chunks
+ .update_view_center(ChunkPos::new(p.x, p.z));
+ });
+ }
+
+ pub fn chunks_biomes(&mut self, _p: &ClientboundChunksBiomes) {}
+
+ pub fn light_update(&mut self, _p: &ClientboundLightUpdate) {
+ // debug!("Got light update packet {p:?}");
+ }
+
+ pub fn level_chunk_with_light(&mut self, p: &ClientboundLevelChunkWithLight) {
+ debug!("Got chunk with light packet {} {}", p.x, p.z);
+
+ as_system::<EventWriter<_>>(self.ecs, |mut events| {
+ events.send(chunks::ReceiveChunkEvent {
+ entity: self.player,
+ packet: p.clone(),
+ });
+ });
+ }
+
+ pub fn add_entity(&mut self, p: &ClientboundAddEntity) {
+ debug!("Got add entity packet {p:?}");
+
+ as_system::<(
+ Commands,
+ Query<(&mut EntityIdIndex, Option<&InstanceName>, Option<&TabList>)>,
+ Query<&mut LoadedBy>,
+ Query<Entity>,
+ Res<InstanceContainer>,
+ ResMut<EntityUuidIndex>,
+ )>(
+ self.ecs,
+ |(
+ mut commands,
+ mut query,
+ mut loaded_by_query,
+ entity_query,
+ instance_container,
+ mut entity_uuid_index,
+ )| {
+ let (mut entity_id_index, instance_name, tab_list) =
+ query.get_mut(self.player).unwrap();
+
+ let entity_id = p.id;
+
+ let Some(instance_name) = instance_name else {
+ warn!("got add player packet but we haven't gotten a login packet yet");
+ return;
+ };
+
+ // check if the entity already exists, and if it does then only add to LoadedBy
+ let instance = instance_container.get(instance_name).unwrap();
+ if let Some(&ecs_entity) = instance.read().entity_by_id.get(&entity_id) {
+ // entity already exists
+ let Ok(mut loaded_by) = loaded_by_query.get_mut(ecs_entity) else {
+ // LoadedBy for this entity isn't in the ecs! figure out what went wrong
+ // and print an error
+
+ let entity_in_ecs = entity_query.get(ecs_entity).is_ok();
+
+ if entity_in_ecs {
+ error!(
+ "LoadedBy for entity {entity_id:?} ({ecs_entity:?}) isn't in the ecs, but the entity is in entity_by_id"
+ );
+ } else {
+ error!(
+ "Entity {entity_id:?} ({ecs_entity:?}) isn't in the ecs, but the entity is in entity_by_id"
+ );
+ }
+ return;
+ };
+ loaded_by.insert(self.player);
+
+ // per-client id index
+ entity_id_index.insert(entity_id, ecs_entity);
+
+ debug!("added to LoadedBy of entity {ecs_entity:?} with id {entity_id:?}");
+ return;
+ };
+
+ // entity doesn't exist in the global index!
+
+ let bundle = p.as_entity_bundle((**instance_name).clone());
+ let mut spawned =
+ commands.spawn((entity_id, LoadedBy(HashSet::from([self.player])), bundle));
+ let ecs_entity: Entity = spawned.id();
+ debug!("spawned entity {ecs_entity:?} with id {entity_id:?}");
+
+ azalea_entity::indexing::add_entity_to_indexes(
+ entity_id,
+ ecs_entity,
+ Some(p.uuid),
+ &mut entity_id_index,
+ &mut entity_uuid_index,
+ &mut instance.write(),
+ );
+
+ // add the GameProfileComponent if the uuid is in the tab list
+ if let Some(tab_list) = tab_list {
+ // (technically this makes it possible for non-player entities to have
+ // GameProfileComponents but the server would have to be doing something
+ // really weird)
+ if let Some(player_info) = tab_list.get(&p.uuid) {
+ spawned.insert(GameProfileComponent(player_info.profile.clone()));
+ }
+ }
+
+ // the bundle doesn't include the default entity metadata so we add that
+ // separately
+ p.apply_metadata(&mut spawned);
+ },
+ );
+ }
+
+ pub fn set_entity_data(&mut self, p: &ClientboundSetEntityData) {
+ as_system::<(
+ Commands,
+ Query<(&EntityIdIndex, &InstanceHolder)>,
+ // this is a separate query since it's applied on the entity id that's being updated
+ // instead of the player that received the packet
+ Query<&EntityKind>,
+ )>(self.ecs, |(mut commands, query, entity_kind_query)| {
+ let (entity_id_index, instance_holder) = query.get(self.player).unwrap();
+
+ let entity = entity_id_index.get(p.id);
+
+ let Some(entity) = entity else {
+ // some servers like hypixel trigger this a lot :(
+ debug!(
+ "Server sent an entity data packet for an entity id ({}) that we don't know about",
+ p.id
+ );
+ return;
+ };
+
+ let entity_kind = *entity_kind_query
+ .get(entity)
+ .expect("EntityKind component should always be present for entities");
+
+ debug!("Got set entity data packet {p:?} for entity of kind {entity_kind:?}");
+
+ let packed_items = p.packed_items.clone().to_vec();
+
+ // we use RelativeEntityUpdate because it makes sure changes aren't made
+ // multiple times
+ commands.entity(entity).queue(RelativeEntityUpdate {
+ partial_world: instance_holder.partial_instance.clone(),
+ update: Box::new(move |entity| {
+ let entity_id = entity.id();
+ entity.world_scope(|world| {
+ let mut commands_system_state = SystemState::<Commands>::new(world);
+ let mut commands = commands_system_state.get_mut(world);
+ let mut entity_commands = commands.entity(entity_id);
+ if let Err(e) =
+ apply_metadata(&mut entity_commands, *entity_kind, packed_items)
+ {
+ warn!("{e}");
+ }
+ commands_system_state.apply(world);
+ });
+ }),
+ });
+ });
+ }
+
+ pub fn update_attributes(&mut self, _p: &ClientboundUpdateAttributes) {
+ // debug!("Got update attributes packet {p:?}");
+ }
+
+ pub fn set_entity_motion(&mut self, p: &ClientboundSetEntityMotion) {
+ // vanilla servers use this packet for knockback, but note that the Explode
+ // packet is also sometimes used by servers for knockback
+
+ as_system::<(Commands, Query<(&EntityIdIndex, &InstanceHolder)>)>(
+ self.ecs,
+ |(mut commands, query)| {
+ let (entity_id_index, instance_holder) = query.get(self.player).unwrap();
+
+ let Some(entity) = entity_id_index.get(p.id) else {
+ // note that this log (and some other ones like the one in RemoveEntities)
+ // sometimes happens when killing mobs. it seems to be a vanilla bug, which is
+ // why it's a debug log instead of a warning
+ debug!(
+ "Got set entity motion packet for unknown entity id {}",
+ p.id
+ );
+ return;
+ };
+
+ // this is to make sure the same entity velocity update doesn't get sent
+ // multiple times when in swarms
+
+ let knockback = KnockbackType::Set(Vec3 {
+ x: p.delta.xa as f64 / 8000.,
+ y: p.delta.ya as f64 / 8000.,
+ z: p.delta.za as f64 / 8000.,
+ });
+
+ commands.entity(entity).queue(RelativeEntityUpdate {
+ partial_world: instance_holder.partial_instance.clone(),
+ update: Box::new(move |entity_mut| {
+ entity_mut.world_scope(|world| {
+ world.send_event(KnockbackEvent { entity, knockback })
+ });
+ }),
+ });
+ },
+ );
+ }
+
+ pub fn set_entity_link(&mut self, p: &ClientboundSetEntityLink) {
+ debug!("Got set entity link packet {p:?}");
+ }
+
+ pub fn initialize_border(&mut self, p: &ClientboundInitializeBorder) {
+ debug!("Got initialize border packet {p:?}");
+ }
+
+ pub fn set_time(&mut self, _p: &ClientboundSetTime) {
+ // debug!("Got set time packet {p:?}");
+ }
+
+ pub fn set_default_spawn_position(&mut self, p: &ClientboundSetDefaultSpawnPosition) {
+ debug!("Got set default spawn position packet {p:?}");
+ }
+
+ pub fn set_health(&mut self, p: &ClientboundSetHealth) {
+ debug!("Got set health packet {p:?}");
+
+ as_system::<Query<(&mut Health, &mut Hunger)>>(self.ecs, |mut query| {
+ let (mut health, mut hunger) = query.get_mut(self.player).unwrap();
+
+ **health = p.health;
+ (hunger.food, hunger.saturation) = (p.food, p.saturation);
+
+ // the `Dead` component is added by the `update_dead` system
+ // in azalea-world and then the `dead_event` system fires
+ // the Death event.
+ });
+ }
+
+ pub fn set_experience(&mut self, p: &ClientboundSetExperience) {
+ debug!("Got set experience packet {p:?}");
+ }
+
+ pub fn teleport_entity(&mut self, p: &ClientboundTeleportEntity) {
+ as_system::<(Commands, Query<(&EntityIdIndex, &InstanceHolder)>)>(
+ self.ecs,
+ |(mut commands, mut query)| {
+ let (entity_id_index, instance_holder) = query.get_mut(self.player).unwrap();
+
+ let Some(entity) = entity_id_index.get(p.id) else {
+ warn!("Got teleport entity packet for unknown entity id {}", p.id);
+ return;
+ };
+
+ let new_pos = p.change.pos;
+ let new_look_direction = LookDirection {
+ x_rot: (p.change.look_direction.x_rot as i32 * 360) as f32 / 256.,
+ y_rot: (p.change.look_direction.y_rot as i32 * 360) as f32 / 256.,
+ };
+ commands.entity(entity).queue(RelativeEntityUpdate {
+ partial_world: instance_holder.partial_instance.clone(),
+ update: Box::new(move |entity| {
+ let mut position = entity.get_mut::<Position>().unwrap();
+ if new_pos != **position {
+ **position = new_pos;
+ }
+ let position = *position;
+ let mut look_direction = entity.get_mut::<LookDirection>().unwrap();
+ if new_look_direction != *look_direction {
+ *look_direction = new_look_direction;
+ }
+ // old_pos is set to the current position when we're teleported
+ let mut physics = entity.get_mut::<Physics>().unwrap();
+ physics.set_old_pos(&position);
+ }),
+ });
+ },
+ );
+ }
+
+ pub fn update_advancements(&mut self, p: &ClientboundUpdateAdvancements) {
+ debug!("Got update advancements packet {p:?}");
+ }
+
+ pub fn rotate_head(&mut self, _p: &ClientboundRotateHead) {}
+
+ pub fn move_entity_pos(&mut self, p: &ClientboundMoveEntityPos) {
+ as_system::<(Commands, Query<(&EntityIdIndex, &InstanceHolder)>)>(
+ self.ecs,
+ |(mut commands, mut query)| {
+ let (entity_id_index, instance_holder) = query.get_mut(self.player).unwrap();
+
+ debug!("Got move entity pos packet {p:?}");
+
+ let Some(entity) = entity_id_index.get(p.entity_id) else {
+ debug!(
+ "Got move entity pos packet for unknown entity id {}",
+ p.entity_id
+ );
+ return;
+ };
+
+ let new_delta = p.delta.clone();
+ let new_on_ground = p.on_ground;
+ commands.entity(entity).queue(RelativeEntityUpdate {
+ partial_world: instance_holder.partial_instance.clone(),
+ update: Box::new(move |entity_mut| {
+ let mut physics = entity_mut.get_mut::<Physics>().unwrap();
+ let new_pos = physics.vec_delta_codec.decode(
+ new_delta.xa as i64,
+ new_delta.ya as i64,
+ new_delta.za as i64,
+ );
+ physics.vec_delta_codec.set_base(new_pos);
+ physics.set_on_ground(new_on_ground);
+
+ let mut position = entity_mut.get_mut::<Position>().unwrap();
+ if new_pos != **position {
+ **position = new_pos;
+ }
+ }),
+ });
+ },
+ );
+ }
+
+ pub fn move_entity_pos_rot(&mut self, p: &ClientboundMoveEntityPosRot) {
+ as_system::<(Commands, Query<(&EntityIdIndex, &InstanceHolder)>)>(
+ self.ecs,
+ |(mut commands, mut query)| {
+ let (entity_id_index, instance_holder) = query.get_mut(self.player).unwrap();
+
+ debug!("Got move entity pos rot packet {p:?}");
+
+ let entity = entity_id_index.get(p.entity_id);
+
+ let Some(entity) = entity else {
+ // often triggered by hypixel :(
+ debug!(
+ "Got move entity pos rot packet for unknown entity id {}",
+ p.entity_id
+ );
+ return;
+ };
+
+ let new_delta = p.delta.clone();
+ let new_look_direction = LookDirection {
+ x_rot: (p.x_rot as i32 * 360) as f32 / 256.,
+ y_rot: (p.y_rot as i32 * 360) as f32 / 256.,
+ };
+
+ let new_on_ground = p.on_ground;
+
+ commands.entity(entity).queue(RelativeEntityUpdate {
+ partial_world: instance_holder.partial_instance.clone(),
+ update: Box::new(move |entity_mut| {
+ let mut physics = entity_mut.get_mut::<Physics>().unwrap();
+ let new_pos = physics.vec_delta_codec.decode(
+ new_delta.xa as i64,
+ new_delta.ya as i64,
+ new_delta.za as i64,
+ );
+ physics.vec_delta_codec.set_base(new_pos);
+ physics.set_on_ground(new_on_ground);
+
+ let mut position = entity_mut.get_mut::<Position>().unwrap();
+ if new_pos != **position {
+ **position = new_pos;
+ }
+
+ let mut look_direction = entity_mut.get_mut::<LookDirection>().unwrap();
+ if new_look_direction != *look_direction {
+ *look_direction = new_look_direction;
+ }
+ }),
+ });
+ },
+ );
+ }
+
+ pub fn move_entity_rot(&mut self, p: &ClientboundMoveEntityRot) {
+ as_system::<(Commands, Query<(&EntityIdIndex, &InstanceHolder)>)>(
+ self.ecs,
+ |(mut commands, mut query)| {
+ let (entity_id_index, instance_holder) = query.get_mut(self.player).unwrap();
+
+ let entity = entity_id_index.get(p.entity_id);
+ if let Some(entity) = entity {
+ let new_look_direction = LookDirection {
+ x_rot: (p.x_rot as i32 * 360) as f32 / 256.,
+ y_rot: (p.y_rot as i32 * 360) as f32 / 256.,
+ };
+ let new_on_ground = p.on_ground;
+
+ commands.entity(entity).queue(RelativeEntityUpdate {
+ partial_world: instance_holder.partial_instance.clone(),
+ update: Box::new(move |entity_mut| {
+ let mut physics = entity_mut.get_mut::<Physics>().unwrap();
+ physics.set_on_ground(new_on_ground);
+
+ let mut look_direction = entity_mut.get_mut::<LookDirection>().unwrap();
+ if new_look_direction != *look_direction {
+ *look_direction = new_look_direction;
+ }
+ }),
+ });
+ } else {
+ warn!(
+ "Got move entity rot packet for unknown entity id {}",
+ p.entity_id
+ );
+ }
+ },
+ );
+ }
+ pub fn keep_alive(&mut self, p: &ClientboundKeepAlive) {
+ debug!("Got keep alive packet {p:?} for {:?}", self.player);
+
+ as_system::<(EventWriter<KeepAliveEvent>, EventWriter<SendPacketEvent>)>(
+ self.ecs,
+ |(mut keepalive_events, mut send_packet_events)| {
+ keepalive_events.send(KeepAliveEvent {
+ entity: self.player,
+ id: p.id,
+ });
+ send_packet_events.send(SendPacketEvent::new(
+ self.player,
+ ServerboundKeepAlive { id: p.id },
+ ));
+ },
+ );
+ }
+
+ pub fn remove_entities(&mut self, p: &ClientboundRemoveEntities) {
+ debug!("Got remove entities packet {p:?}");
+
+ as_system::<(Query<&mut EntityIdIndex>, Query<&mut LoadedBy>)>(
+ self.ecs,
+ |(mut query, mut entity_query)| {
+ let Ok(mut entity_id_index) = query.get_mut(self.player) else {
+ warn!("our local player doesn't have EntityIdIndex");
+ return;
+ };
+
+ for &id in &p.entity_ids {
+ let Some(entity) = entity_id_index.remove(id) else {
+ debug!(
+ "Tried to remove entity with id {id} but it wasn't in the EntityIdIndex"
+ );
+ continue;
+ };
+ let Ok(mut loaded_by) = entity_query.get_mut(entity) else {
+ warn!(
+ "tried to despawn entity {id} but it doesn't have a LoadedBy component",
+ );
+ continue;
+ };
+
+ // the [`remove_despawned_entities_from_indexes`] system will despawn the entity
+ // if it's not loaded by anything anymore
+
+ // also we can't just ecs.despawn because if we're in a swarm then the entity
+ // might still be loaded by another client
+
+ loaded_by.remove(&self.player);
+ }
+ },
+ );
+ }
+ pub fn player_chat(&mut self, p: &ClientboundPlayerChat) {
+ debug!("Got player chat packet {p:?}");
+
+ as_system::<EventWriter<_>>(self.ecs, |mut events| {
+ events.send(ChatReceivedEvent {
+ entity: self.player,
+ packet: ChatPacket::Player(Arc::new(p.clone())),
+ });
+ });
+ }
+
+ pub fn system_chat(&mut self, p: &ClientboundSystemChat) {
+ debug!("Got system chat packet {p:?}");
+
+ as_system::<EventWriter<_>>(self.ecs, |mut events| {
+ events.send(ChatReceivedEvent {
+ entity: self.player,
+ packet: ChatPacket::System(Arc::new(p.clone())),
+ });
+ });
+ }
+
+ pub fn disguised_chat(&mut self, p: &ClientboundDisguisedChat) {
+ debug!("Got disguised chat packet {p:?}");
+
+ as_system::<EventWriter<_>>(self.ecs, |mut events| {
+ events.send(ChatReceivedEvent {
+ entity: self.player,
+ packet: ChatPacket::Disguised(Arc::new(p.clone())),
+ });
+ });
+ }
+
+ pub fn sound(&mut self, _p: &ClientboundSound) {}
+
+ pub fn level_event(&mut self, p: &ClientboundLevelEvent) {
+ debug!("Got level event packet {p:?}");
+ }
+
+ pub fn block_update(&mut self, p: &ClientboundBlockUpdate) {
+ debug!("Got block update packet {p:?}");
+
+ as_system::<Query<&mut InstanceHolder>>(self.ecs, |mut query| {
+ let local_player = query.get_mut(self.player).unwrap();
+
+ let world = local_player.instance.write();
+
+ world.chunks.set_block_state(&p.pos, p.block_state);
+ });
+ }
+
+ pub fn animate(&mut self, p: &ClientboundAnimate) {
+ debug!("Got animate packet {p:?}");
+ }
+
+ pub fn section_blocks_update(&mut self, p: &ClientboundSectionBlocksUpdate) {
+ debug!("Got section blocks update packet {p:?}");
+
+ as_system::<Query<&mut InstanceHolder>>(self.ecs, |mut query| {
+ let local_player = query.get_mut(self.player).unwrap();
+ let world = local_player.instance.write();
+ for state in &p.states {
+ world
+ .chunks
+ .set_block_state(&(p.section_pos + state.pos), state.state);
+ }
+ });
+ }
+
+ pub fn game_event(&mut self, p: &ClientboundGameEvent) {
+ use azalea_protocol::packets::game::c_game_event::EventType;
+
+ debug!("Got game event packet {p:?}");
+
+ #[allow(clippy::single_match)]
+ match p.event {
+ EventType::ChangeGameMode => {
+ as_system::<Query<&mut LocalGameMode>>(self.ecs, |mut query| {
+ let mut local_game_mode = query.get_mut(self.player).unwrap();
+ if let Some(new_game_mode) = GameMode::from_id(p.param as u8) {
+ local_game_mode.current = new_game_mode;
+ }
+ });
+ }
+ _ => {}
+ }
+ }
+
+ pub fn level_particles(&mut self, p: &ClientboundLevelParticles) {
+ debug!("Got level particles packet {p:?}");
+ }
+
+ pub fn server_data(&mut self, p: &ClientboundServerData) {
+ debug!("Got server data packet {p:?}");
+ }
+
+ pub fn set_equipment(&mut self, p: &ClientboundSetEquipment) {
+ debug!("Got set equipment packet {p:?}");
+ }
+
+ pub fn update_mob_effect(&mut self, p: &ClientboundUpdateMobEffect) {
+ debug!("Got update mob effect packet {p:?}");
+ }
+
+ pub fn add_experience_orb(&mut self, _p: &ClientboundAddExperienceOrb) {}
+
+ pub fn award_stats(&mut self, _p: &ClientboundAwardStats) {}
+
+ pub fn block_changed_ack(&mut self, _p: &ClientboundBlockChangedAck) {}
+
+ pub fn block_destruction(&mut self, _p: &ClientboundBlockDestruction) {}
+
+ pub fn block_entity_data(&mut self, _p: &ClientboundBlockEntityData) {}
+
+ pub fn block_event(&mut self, p: &ClientboundBlockEvent) {
+ debug!("Got block event packet {p:?}");
+ }
+
+ pub fn boss_event(&mut self, _p: &ClientboundBossEvent) {}
+
+ pub fn command_suggestions(&mut self, _p: &ClientboundCommandSuggestions) {}
+
+ pub fn container_set_content(&mut self, p: &ClientboundContainerSetContent) {
+ debug!("Got container set content packet {p:?}");
+
+ as_system::<(Query<&mut Inventory>, EventWriter<_>)>(
+ self.ecs,
+ |(mut query, mut events)| {
+ let mut inventory = query.get_mut(self.player).unwrap();
+
+ // container id 0 is always the player's inventory
+ if p.container_id == 0 {
+ // this is just so it has the same type as the `else` block
+ for (i, slot) in p.items.iter().enumerate() {
+ if let Some(slot_mut) = inventory.inventory_menu.slot_mut(i) {
+ *slot_mut = slot.clone();
+ }
+ }
+ } else {
+ events.send(SetContainerContentEvent {
+ entity: self.player,
+ slots: p.items.clone(),
+ container_id: p.container_id,
+ });
+ }
+ },
+ );
+ }
+
+ pub fn container_set_data(&mut self, p: &ClientboundContainerSetData) {
+ debug!("Got container set data packet {p:?}");
+
+ // TODO: handle ContainerSetData packet
+ // this is used for various things like the furnace progress
+ // bar
+ // see https://wiki.vg/Protocol#Set_Container_Property
+
+ // as_system::<Query<&mut Inventory>>(self.ecs, |mut query| {
+ // let inventory = query.get_mut(self.player).unwrap();
+ // });
+ }
+
+ pub fn container_set_slot(&mut self, p: &ClientboundContainerSetSlot) {
+ debug!("Got container set slot packet {p:?}");
+
+ as_system::<Query<&mut Inventory>>(self.ecs, |mut query| {
+ let mut inventory = query.get_mut(self.player).unwrap();
+
+ if p.container_id == -1 {
+ // -1 means carried item
+ inventory.carried = p.item_stack.clone();
+ } else if p.container_id == -2 {
+ if let Some(slot) = inventory.inventory_menu.slot_mut(p.slot.into()) {
+ *slot = p.item_stack.clone();
+ }
+ } else {
+ let is_creative_mode_and_inventory_closed = false;
+ // technically minecraft has slightly different behavior here if you're in
+ // creative mode and have your inventory open
+ if p.container_id == 0 && azalea_inventory::Player::is_hotbar_slot(p.slot.into()) {
+ // minecraft also sets a "pop time" here which is used for an animation
+ // but that's not really necessary
+ if let Some(slot) = inventory.inventory_menu.slot_mut(p.slot.into()) {
+ *slot = p.item_stack.clone();
+ }
+ } else if p.container_id == inventory.id
+ && (p.container_id != 0 || !is_creative_mode_and_inventory_closed)
+ {
+ // var2.containerMenu.setItem(var4, var1.getStateId(), var3);
+ if let Some(slot) = inventory.menu_mut().slot_mut(p.slot.into()) {
+ *slot = p.item_stack.clone();
+ inventory.state_id = p.state_id;
+ }
+ }
+ }
+ });
+ }
+
+ pub fn container_close(&mut self, p: &ClientboundContainerClose) {
+ // there's a container_id field in the packet, but minecraft doesn't actually
+ // check it
+
+ debug!("Got container close packet {p:?}");
+
+ as_system::<EventWriter<_>>(self.ecs, |mut events| {
+ events.send(ClientSideCloseContainerEvent {
+ entity: self.player,
+ });
+ });
+ }
+
+ pub fn cooldown(&mut self, _p: &ClientboundCooldown) {}
+
+ pub fn custom_chat_completions(&mut self, _p: &ClientboundCustomChatCompletions) {}
+
+ pub fn delete_chat(&mut self, _p: &ClientboundDeleteChat) {}
+
+ pub fn explode(&mut self, p: &ClientboundExplode) {
+ trace!("Got explode packet {p:?}");
+
+ as_system::<EventWriter<_>>(self.ecs, |mut knockback_events| {
+ if let Some(knockback) = p.knockback {
+ knockback_events.send(KnockbackEvent {
+ entity: self.player,
+ knockback: KnockbackType::Set(knockback),
+ });
+ }
+ });
+ }
+
+ pub fn forget_level_chunk(&mut self, p: &ClientboundForgetLevelChunk) {
+ debug!("Got forget level chunk packet {p:?}");
+
+ as_system::<Query<&mut InstanceHolder>>(self.ecs, |mut query| {
+ let local_player = query.get_mut(self.player).unwrap();
+
+ let mut partial_instance = local_player.partial_instance.write();
+
+ partial_instance.chunks.limited_set(&p.pos, None);
+ });
+ }
+
+ pub fn horse_screen_open(&mut self, _p: &ClientboundHorseScreenOpen) {}
+
+ pub fn map_item_data(&mut self, _p: &ClientboundMapItemData) {}
+
+ pub fn merchant_offers(&mut self, _p: &ClientboundMerchantOffers) {}
+
+ pub fn move_vehicle(&mut self, _p: &ClientboundMoveVehicle) {}
+
+ pub fn open_book(&mut self, _p: &ClientboundOpenBook) {}
+
+ pub fn open_screen(&mut self, p: &ClientboundOpenScreen) {
+ debug!("Got open screen packet {p:?}");
+
+ as_system::<EventWriter<_>>(self.ecs, |mut events| {
+ events.send(MenuOpenedEvent {
+ entity: self.player,
+ window_id: p.container_id,
+ menu_type: p.menu_type,
+ title: p.title.to_owned(),
+ });
+ });
+ }
+
+ pub fn open_sign_editor(&mut self, _p: &ClientboundOpenSignEditor) {}
+
+ pub fn ping(&mut self, p: &ClientboundPing) {
+ debug!("Got ping packet {p:?}");
+
+ as_system::<EventWriter<_>>(self.ecs, |mut events| {
+ events.send(SendPacketEvent::new(
+ self.player,
+ ServerboundPong { id: p.id },
+ ));
+ });
+ }
+
+ pub fn place_ghost_recipe(&mut self, _p: &ClientboundPlaceGhostRecipe) {}
+
+ pub fn player_combat_end(&mut self, _p: &ClientboundPlayerCombatEnd) {}
+
+ pub fn player_combat_enter(&mut self, _p: &ClientboundPlayerCombatEnter) {}
+
+ pub fn player_combat_kill(&mut self, p: &ClientboundPlayerCombatKill) {
+ debug!("Got player kill packet {p:?}");
+
+ as_system::<(
+ Commands,
+ Query<(&MinecraftEntityId, Option<&Dead>)>,
+ EventWriter<_>,
+ )>(self.ecs, |(mut commands, mut query, mut events)| {
+ let (entity_id, dead) = query.get_mut(self.player).unwrap();
+
+ if *entity_id == p.player_id && dead.is_none() {
+ commands.entity(self.player).insert(Dead);
+ events.send(DeathEvent {
+ entity: self.player,
+ packet: Some(p.clone()),
+ });
+ }
+ });
+ }
+
+ pub fn player_look_at(&mut self, _p: &ClientboundPlayerLookAt) {}
+
+ pub fn remove_mob_effect(&mut self, _p: &ClientboundRemoveMobEffect) {}
+
+ pub fn resource_pack_push(&mut self, p: &ClientboundResourcePackPush) {
+ debug!("Got resource pack packet {p:?}");
+
+ as_system::<EventWriter<_>>(self.ecs, |mut events| {
+ events.send(ResourcePackEvent {
+ entity: self.player,
+ id: p.id,
+ url: p.url.to_owned(),
+ hash: p.hash.to_owned(),
+ required: p.required,
+ prompt: p.prompt.to_owned(),
+ });
+ });
+ }
+
+ pub fn resource_pack_pop(&mut self, _p: &ClientboundResourcePackPop) {}
+
+ pub fn respawn(&mut self, p: &ClientboundRespawn) {
+ debug!("Got respawn packet {p:?}");
+
+ as_system::<(
+ Commands,
+ Query<(
+ &mut InstanceHolder,
+ &GameProfileComponent,
+ &ClientInformation,
+ )>,
+ EventWriter<_>,
+ ResMut<InstanceContainer>,
+ )>(
+ self.ecs,
+ |(mut commands, mut query, mut events, mut instance_container)| {
+ let (mut instance_holder, game_profile, client_information) =
+ query.get_mut(self.player).unwrap();
+
+ let new_instance_name = p.common.dimension.clone();
+
+ let Some((_dimension_type, dimension_data)) = p
+ .common
+ .dimension_type(&instance_holder.instance.read().registries)
+ else {
+ return;
+ };
+
+ // add this world to the instance_container (or don't if it's already
+ // there)
+ let weak_instance = instance_container.insert(
+ new_instance_name.clone(),
+ dimension_data.height,
+ dimension_data.min_y,
+ &instance_holder.instance.read().registries,
+ );
+ events.send(InstanceLoadedEvent {
+ entity: self.player,
+ name: new_instance_name.clone(),
+ instance: Arc::downgrade(&weak_instance),
+ });
+
+ // set the partial_world to an empty world
+ // (when we add chunks or entities those will be in the
+ // instance_container)
+
+ *instance_holder.partial_instance.write() = PartialInstance::new(
+ azalea_world::chunk_storage::calculate_chunk_storage_range(
+ client_information.view_distance.into(),
+ ),
+ Some(self.player),
+ );
+ instance_holder.instance = weak_instance;
+
+ // this resets a bunch of our components like physics and stuff
+ let entity_bundle = EntityBundle::new(
+ game_profile.uuid,
+ Vec3::default(),
+ azalea_registry::EntityKind::Player,
+ new_instance_name,
+ );
+ // update the local gamemode and metadata things
+ commands.entity(self.player).insert((
+ LocalGameMode {
+ current: p.common.game_type,
+ previous: p.common.previous_game_type.into(),
+ },
+ entity_bundle,
+ ));
+
+ // Remove the Dead marker component from the player.
+ commands.entity(self.player).remove::<Dead>();
+ },
+ )
+ }
+
+ pub fn start_configuration(&mut self, _p: &ClientboundStartConfiguration) {
+ as_system::<(Commands, EventWriter<_>)>(self.ecs, |(mut commands, mut events)| {
+ events.send(SendPacketEvent::new(
+ self.player,
+ ServerboundConfigurationAcknowledged {},
+ ));
+
+ commands
+ .entity(self.player)
+ .insert(crate::client::InConfigState)
+ .remove::<crate::JoinedClientBundle>();
+ });
+ }
+
+ pub fn entity_position_sync(&mut self, p: &ClientboundEntityPositionSync) {
+ as_system::<(Commands, Query<(&EntityIdIndex, &InstanceHolder)>)>(
+ self.ecs,
+ |(mut commands, mut query)| {
+ let (entity_id_index, instance_holder) = query.get_mut(self.player).unwrap();
+
+ let Some(entity) = entity_id_index.get(p.id) else {
+ debug!("Got teleport entity packet for unknown entity id {}", p.id);
+ return;
+ };
+
+ let new_position = p.values.pos;
+ let new_on_ground = p.on_ground;
+ let new_look_direction = p.values.look_direction;
+
+ commands.entity(entity).queue(RelativeEntityUpdate {
+ partial_world: instance_holder.partial_instance.clone(),
+ update: Box::new(move |entity_mut| {
+ let is_local_entity = entity_mut.get::<LocalEntity>().is_some();
+ let mut physics = entity_mut.get_mut::<Physics>().unwrap();
+
+ physics.vec_delta_codec.set_base(new_position);
+
+ if is_local_entity {
+ debug!("Ignoring entity position sync packet for local player");
+ return;
+ }
+
+ physics.set_on_ground(new_on_ground);
+
+ let mut last_sent_position =
+ entity_mut.get_mut::<LastSentPosition>().unwrap();
+ **last_sent_position = new_position;
+ let mut position = entity_mut.get_mut::<Position>().unwrap();
+ **position = new_position;
+
+ let mut look_direction = entity_mut.get_mut::<LookDirection>().unwrap();
+ *look_direction = new_look_direction;
+ }),
+ });
+ },
+ );
+ }
+
+ pub fn select_advancements_tab(&mut self, _p: &ClientboundSelectAdvancementsTab) {}
+ pub fn set_action_bar_text(&mut self, _p: &ClientboundSetActionBarText) {}
+ pub fn set_border_center(&mut self, _p: &ClientboundSetBorderCenter) {}
+ pub fn set_border_lerp_size(&mut self, _p: &ClientboundSetBorderLerpSize) {}
+ pub fn set_border_size(&mut self, _p: &ClientboundSetBorderSize) {}
+ pub fn set_border_warning_delay(&mut self, _p: &ClientboundSetBorderWarningDelay) {}
+ pub fn set_border_warning_distance(&mut self, _p: &ClientboundSetBorderWarningDistance) {}
+ pub fn set_camera(&mut self, _p: &ClientboundSetCamera) {}
+ pub fn set_display_objective(&mut self, _p: &ClientboundSetDisplayObjective) {}
+ pub fn set_objective(&mut self, _p: &ClientboundSetObjective) {}
+ pub fn set_passengers(&mut self, _p: &ClientboundSetPassengers) {}
+ pub fn set_player_team(&mut self, _p: &ClientboundSetPlayerTeam) {}
+ pub fn set_score(&mut self, _p: &ClientboundSetScore) {}
+ pub fn set_simulation_distance(&mut self, _p: &ClientboundSetSimulationDistance) {}
+ pub fn set_subtitle_text(&mut self, _p: &ClientboundSetSubtitleText) {}
+ pub fn set_title_text(&mut self, _p: &ClientboundSetTitleText) {}
+ pub fn set_titles_animation(&mut self, _p: &ClientboundSetTitlesAnimation) {}
+ pub fn clear_titles(&mut self, _p: &ClientboundClearTitles) {}
+ pub fn sound_entity(&mut self, _p: &ClientboundSoundEntity) {}
+ pub fn stop_sound(&mut self, _p: &ClientboundStopSound) {}
+ pub fn tab_list(&mut self, _p: &ClientboundTabList) {}
+ pub fn tag_query(&mut self, _p: &ClientboundTagQuery) {}
+ pub fn take_item_entity(&mut self, _p: &ClientboundTakeItemEntity) {}
+ pub fn bundle_delimiter(&mut self, _p: &ClientboundBundleDelimiter) {}
+ pub fn damage_event(&mut self, _p: &ClientboundDamageEvent) {}
+ pub fn hurt_animation(&mut self, _p: &ClientboundHurtAnimation) {}
+ pub fn ticking_state(&mut self, _p: &ClientboundTickingState) {}
+ pub fn ticking_step(&mut self, _p: &ClientboundTickingStep) {}
+ pub fn reset_score(&mut self, _p: &ClientboundResetScore) {}
+ pub fn cookie_request(&mut self, _p: &ClientboundCookieRequest) {}
+ pub fn debug_sample(&mut self, _p: &ClientboundDebugSample) {}
+ pub fn pong_response(&mut self, _p: &ClientboundPongResponse) {}
+ pub fn store_cookie(&mut self, _p: &ClientboundStoreCookie) {}
+ pub fn transfer(&mut self, _p: &ClientboundTransfer) {}
+ pub fn move_minecart_along_track(&mut self, _p: &ClientboundMoveMinecartAlongTrack) {}
+ pub fn set_held_slot(&mut self, _p: &ClientboundSetHeldSlot) {}
+ pub fn set_player_inventory(&mut self, _p: &ClientboundSetPlayerInventory) {}
+ pub fn projectile_power(&mut self, _p: &ClientboundProjectilePower) {}
+ pub fn custom_report_details(&mut self, _p: &ClientboundCustomReportDetails) {}
+ pub fn server_links(&mut self, _p: &ClientboundServerLinks) {}
+ pub fn player_rotation(&mut self, _p: &ClientboundPlayerRotation) {}
+ pub fn recipe_book_add(&mut self, _p: &ClientboundRecipeBookAdd) {}
+ pub fn recipe_book_remove(&mut self, _p: &ClientboundRecipeBookRemove) {}
+ pub fn recipe_book_settings(&mut self, _p: &ClientboundRecipeBookSettings) {}
+}
diff --git a/azalea-client/src/packet_handling/login.rs b/azalea-client/src/plugins/packet/login.rs
index 8cf45afc..1bb07266 100644
--- a/azalea-client/src/packet_handling/login.rs
+++ b/azalea-client/src/plugins/packet/login.rs
@@ -20,7 +20,7 @@ use tracing::error;
/// An event that's sent when we receive a login packet from the server. Note
/// that if you want to handle this in a system, you must add
-/// `.before(azalea::packet_handling::login::process_packet_events)` to it
+/// `.before(azalea::packet::login::process_packet_events)` to it
/// because that system clears the events.
#[derive(Event, Debug, Clone)]
pub struct LoginPacketEvent {
diff --git a/azalea-client/src/packet_handling/mod.rs b/azalea-client/src/plugins/packet/mod.rs
index 908f368e..cbd8a175 100644
--- a/azalea-client/src/packet_handling/mod.rs
+++ b/azalea-client/src/plugins/packet/mod.rs
@@ -1,6 +1,9 @@
use azalea_entity::{EntityUpdateSet, metadata::Health};
use bevy_app::{App, First, Plugin, PreUpdate, Update};
-use bevy_ecs::prelude::*;
+use bevy_ecs::{
+ prelude::*,
+ system::{SystemParam, SystemState},
+};
use self::{
game::{
@@ -11,11 +14,11 @@ use self::{
};
use crate::{chat::ChatReceivedEvent, events::death_listener};
-pub mod configuration;
+pub mod config;
pub mod game;
pub mod login;
-pub struct PacketHandlerPlugin;
+pub struct PacketPlugin;
pub fn death_event_on_0_health(
query: Query<(Entity, &Health), Changed<Health>>,
@@ -31,11 +34,11 @@ pub fn death_event_on_0_health(
}
}
-impl Plugin for PacketHandlerPlugin {
+impl Plugin for PacketPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
First,
- (game::send_packet_events, configuration::send_packet_events),
+ (game::send_receivepacketevent, config::send_packet_events),
)
.add_systems(
PreUpdate,
@@ -43,7 +46,7 @@ impl Plugin for PacketHandlerPlugin {
game::process_packet_events
// we want to index and deindex right after
.before(EntityUpdateSet::Deindex),
- configuration::process_packet_events,
+ config::process_packet_events,
login::handle_send_packet_event,
login::process_packet_events,
),
@@ -52,18 +55,18 @@ impl Plugin for PacketHandlerPlugin {
Update,
(
(
- configuration::handle_send_packet_event,
- game::handle_send_packet_event,
+ config::handle_send_packet_event,
+ game::handle_outgoing_packets,
)
.chain(),
death_event_on_0_health.before(death_listener),
),
)
// we do this instead of add_event so we can handle the events ourselves
- .init_resource::<Events<game::PacketEvent>>()
- .init_resource::<Events<configuration::ConfigurationEvent>>()
+ .init_resource::<Events<game::ReceivePacketEvent>>()
+ .init_resource::<Events<config::ReceiveConfigPacketEvent>>()
.add_event::<game::SendPacketEvent>()
- .add_event::<configuration::SendConfigurationEvent>()
+ .add_event::<config::SendConfigPacketEvent>()
.add_event::<AddPlayerEvent>()
.add_event::<RemovePlayerEvent>()
.add_event::<UpdatePlayerEvent>()
@@ -76,3 +79,31 @@ impl Plugin for PacketHandlerPlugin {
.add_event::<SendLoginPacketEvent>();
}
}
+
+#[macro_export]
+macro_rules! declare_packet_handlers {
+ (
+ $packetenum:ident,
+ $packetvar:expr,
+ $handler:ident,
+ [$($packet:path),+ $(,)?]
+ ) => {
+ paste::paste! {
+ match $packetvar {
+ $(
+ $packetenum::[< $packet:camel >](p) => $handler.$packet(p),
+ )+
+ }
+ }
+ };
+}
+
+pub(crate) fn as_system<T>(ecs: &mut World, f: impl FnOnce(T::Item<'_, '_>))
+where
+ T: SystemParam + 'static,
+{
+ let mut system_state = SystemState::<T>::new(ecs);
+ let values = system_state.get_mut(ecs);
+ f(values);
+ system_state.apply(ecs);
+}
diff --git a/azalea-client/src/respawn.rs b/azalea-client/src/plugins/respawn.rs
index 41d1c470..5797406b 100644
--- a/azalea-client/src/respawn.rs
+++ b/azalea-client/src/plugins/respawn.rs
@@ -2,7 +2,8 @@ use azalea_protocol::packets::game::s_client_command::{self, ServerboundClientCo
use bevy_app::{App, Plugin, Update};
use bevy_ecs::prelude::*;
-use crate::packet_handling::game::{SendPacketEvent, handle_send_packet_event};
+use super::packet::game::handle_outgoing_packets;
+use crate::packet::game::SendPacketEvent;
/// Tell the server that we're respawning.
#[derive(Event, Debug, Clone)]
@@ -15,7 +16,7 @@ pub struct RespawnPlugin;
impl Plugin for RespawnPlugin {
fn build(&self, app: &mut App) {
app.add_event::<PerformRespawnEvent>()
- .add_systems(Update, perform_respawn.before(handle_send_packet_event));
+ .add_systems(Update, perform_respawn.before(handle_outgoing_packets));
}
}
diff --git a/azalea-client/src/task_pool.rs b/azalea-client/src/plugins/task_pool.rs
index ab56bf69..ab56bf69 100644
--- a/azalea-client/src/task_pool.rs
+++ b/azalea-client/src/plugins/task_pool.rs
diff --git a/azalea-client/src/send_client_end.rs b/azalea-client/src/plugins/tick_end.rs
index cb3d5e74..c7737eb1 100644
--- a/azalea-client/src/send_client_end.rs
+++ b/azalea-client/src/plugins/tick_end.rs
@@ -8,7 +8,7 @@ use azalea_world::InstanceName;
use bevy_app::{App, Plugin};
use bevy_ecs::prelude::*;
-use crate::{mining::MiningSet, packet_handling::game::SendPacketEvent};
+use crate::{mining::MiningSet, packet::game::SendPacketEvent};
/// A plugin that makes clients send a [`ServerboundClientTickEnd`] packet every
/// tick.