aboutsummaryrefslogtreecommitdiff
path: root/azalea-client/src/plugins/packet
diff options
context:
space:
mode:
authormat <git@matdoes.dev>2026-02-22 11:35:41 +0700
committermat <git@matdoes.dev>2026-02-22 11:35:41 +0700
commit2756eb419af2210eed3d0574e20a620918e4e577 (patch)
tree0d209e70f65db16b7b982c3e941355589b7d5181 /azalea-client/src/plugins/packet
parentca2fbe329b0879496cf12de4e85d81ca3aa3c351 (diff)
downloadazalea-drasl-2756eb419af2210eed3d0574e20a620918e4e577.tar.xz
optimizations at high entity counts
Diffstat (limited to 'azalea-client/src/plugins/packet')
-rw-r--r--azalea-client/src/plugins/packet/game/mod.rs142
-rw-r--r--azalea-client/src/plugins/packet/mod.rs50
-rw-r--r--azalea-client/src/plugins/packet/relative_updates.rs163
3 files changed, 279 insertions, 76 deletions
diff --git a/azalea-client/src/plugins/packet/game/mod.rs b/azalea-client/src/plugins/packet/game/mod.rs
index 9a1c022b..9f16c204 100644
--- a/azalea-client/src/plugins/packet/game/mod.rs
+++ b/azalea-client/src/plugins/packet/game/mod.rs
@@ -9,7 +9,7 @@ use azalea_core::{
};
use azalea_entity::{
ActiveEffects, Dead, EntityBundle, EntityKindComponent, HasClientLoaded, LoadedBy, LocalEntity,
- LookDirection, Physics, PlayerAbilities, Position, RelativeEntityUpdate,
+ LookDirection, Physics, PlayerAbilities, Position,
indexing::{EntityIdIndex, EntityUuidIndex},
inventory::Inventory,
metadata::{Health, apply_metadata},
@@ -36,7 +36,10 @@ use crate::{
inventory::{ClientsideCloseContainerEvent, MenuOpenedEvent, SetContainerContentEvent},
local_player::{Experience, Hunger, LocalGameMode, TabList, WorldHolder},
movement::{KnockbackData, KnockbackEvent},
- packet::{as_system, declare_packet_handlers},
+ packet::{
+ as_system, declare_packet_handlers,
+ relative_updates::{EntityUpdateQuery, RelativeEntityUpdate, should_apply_entity_update},
+ },
player::{GameProfileComponent, PlayerInfo},
tick_counter::TicksConnected,
};
@@ -716,36 +719,37 @@ impl GamePacketHandler<'_> {
// 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, &WorldHolder)>)>(
- self.ecs,
- |(mut commands, query)| {
- let (entity_id_index, world_holder) = query.get(self.player).unwrap();
-
- let Some(entity) = entity_id_index.get_by_minecraft_entity(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
+ as_system::<(
+ Commands,
+ Query<(&EntityIdIndex, &WorldHolder)>,
+ EntityUpdateQuery,
+ )>(self.ecs, |(mut commands, query, entity_update_query)| {
+ let (entity_id_index, world_holder) = query.get(self.player).unwrap();
- let data = KnockbackData::Set(p.delta.to_vec3());
+ let Some(entity) = entity_id_index.get_by_minecraft_entity(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;
+ };
- commands.entity(entity).queue(RelativeEntityUpdate::new(
- world_holder.partial.clone(),
- move |entity_mut| {
- entity_mut
- .world_scope(|world| world.trigger(KnockbackEvent { entity, data }));
- },
- ));
- },
- );
+ let data = KnockbackData::Set(p.delta.to_vec3());
+
+ // this is to make sure the same entity velocity update doesn't get sent
+ // multiple times when in swarms
+ if should_apply_entity_update(
+ &mut commands,
+ &mut world_holder.partial.write(),
+ entity,
+ entity_update_query,
+ ) {
+ commands.trigger(KnockbackEvent { entity, data });
+ }
+ });
}
pub fn set_entity_link(&mut self, p: &ClientboundSetEntityLink) {
@@ -795,8 +799,8 @@ impl GamePacketHandler<'_> {
as_system::<(Commands, Query<(&EntityIdIndex, &WorldHolder)>)>(
self.ecs,
- |(mut commands, mut query)| {
- let (entity_id_index, world_holder) = query.get_mut(self.player).unwrap();
+ |(mut commands, query)| {
+ let (entity_id_index, world_holder) = query.get(self.player).unwrap();
let Some(entity) = entity_id_index.get_by_minecraft_entity(p.id) else {
warn!("Got teleport entity packet for unknown entity id {}", p.id);
@@ -840,8 +844,8 @@ impl GamePacketHandler<'_> {
pub fn move_entity_pos(&mut self, p: &ClientboundMoveEntityPos) {
as_system::<(Commands, Query<(&EntityIdIndex, &WorldHolder)>)>(
self.ecs,
- |(mut commands, mut query)| {
- let (entity_id_index, world_holder) = query.get_mut(self.player).unwrap();
+ |(mut commands, query)| {
+ let (entity_id_index, world_holder) = query.get(self.player).unwrap();
debug!("Got move entity pos packet {p:?}");
@@ -879,8 +883,8 @@ impl GamePacketHandler<'_> {
pub fn move_entity_pos_rot(&mut self, p: &ClientboundMoveEntityPosRot) {
as_system::<(Commands, Query<(&EntityIdIndex, &WorldHolder)>)>(
self.ecs,
- |(mut commands, mut query)| {
- let (entity_id_index, world_holder) = query.get_mut(self.player).unwrap();
+ |(mut commands, query)| {
+ let (entity_id_index, world_holder) = query.get(self.player).unwrap();
debug!("Got move entity pos rot packet {p:?}");
@@ -929,8 +933,8 @@ impl GamePacketHandler<'_> {
pub fn move_entity_rot(&mut self, p: &ClientboundMoveEntityRot) {
as_system::<(Commands, Query<(&EntityIdIndex, &WorldHolder)>)>(
self.ecs,
- |(mut commands, mut query)| {
- let (entity_id_index, world_holder) = query.get_mut(self.player).unwrap();
+ |(mut commands, query)| {
+ let (entity_id_index, world_holder) = query.get(self.player).unwrap();
let entity = entity_id_index.get_by_minecraft_entity(p.entity_id);
if let Some(entity) = entity {
@@ -1527,10 +1531,28 @@ impl GamePacketHandler<'_> {
}
pub fn entity_position_sync(&mut self, p: &ClientboundEntityPositionSync) {
- as_system::<(Commands, Query<(&EntityIdIndex, &WorldHolder)>)>(
+ as_system::<(
+ Commands,
+ Query<(
+ &EntityIdIndex,
+ &WorldHolder,
+ Option<&LocalEntity>,
+ &mut Physics,
+ &mut Position,
+ &mut LookDirection,
+ )>,
+ EntityUpdateQuery,
+ )>(
self.ecs,
- |(mut commands, mut query)| {
- let (entity_id_index, world_holder) = query.get_mut(self.player).unwrap();
+ |(mut commands, mut query, entity_update_query)| {
+ let (
+ entity_id_index,
+ world_holder,
+ local_entity,
+ mut physics,
+ mut position,
+ mut look_direction,
+ ) = query.get_mut(self.player).unwrap();
let Some(entity) = entity_id_index.get_by_minecraft_entity(p.id) else {
debug!("Got teleport entity packet for unknown entity id {}", p.id);
@@ -1541,28 +1563,32 @@ impl GamePacketHandler<'_> {
let new_on_ground = p.on_ground;
let new_look_direction = p.values.look_direction;
- commands.entity(entity).queue(RelativeEntityUpdate::new(
- world_holder.partial.clone(),
- move |entity_mut| {
- let is_local_entity = entity_mut.get::<LocalEntity>().is_some();
- let mut physics = entity_mut.get_mut::<Physics>().unwrap();
+ if !should_apply_entity_update(
+ &mut commands,
+ &mut world_holder.partial.write(),
+ entity,
+ entity_update_query,
+ ) {
+ return;
+ }
+ let is_local_entity = local_entity.is_some();
- physics.vec_delta_codec.set_base(new_position);
+ physics.vec_delta_codec.set_base(new_position);
- if is_local_entity {
- debug!("Ignoring entity position sync packet for local player");
- return;
- }
+ if is_local_entity {
+ debug!("Ignoring entity position sync packet for local player");
+ return;
+ }
- physics.set_on_ground(new_on_ground);
+ physics.set_on_ground(new_on_ground);
- let mut position = entity_mut.get_mut::<Position>().unwrap();
- **position = new_position;
+ if **position != new_position {
+ **position = new_position;
+ }
- let mut look_direction = entity_mut.get_mut::<LookDirection>().unwrap();
- *look_direction = new_look_direction;
- },
- ));
+ if *look_direction != new_look_direction {
+ *look_direction = new_look_direction;
+ }
},
);
}
diff --git a/azalea-client/src/plugins/packet/mod.rs b/azalea-client/src/plugins/packet/mod.rs
index 63a94ee0..4309850e 100644
--- a/azalea-client/src/plugins/packet/mod.rs
+++ b/azalea-client/src/plugins/packet/mod.rs
@@ -11,6 +11,7 @@ use crate::chat::ChatReceivedEvent;
pub mod config;
pub mod game;
pub mod login;
+pub mod relative_updates;
pub struct PacketPlugin;
@@ -30,23 +31,27 @@ pub fn death_event_on_0_health(
impl Plugin for PacketPlugin {
fn build(&self, app: &mut App) {
- app.add_observer(game::handle_outgoing_packets_observer)
- .add_observer(config::handle_outgoing_packets_observer)
- .add_observer(login::handle_outgoing_packets_observer)
- .add_systems(Update, death_event_on_0_health)
- .add_message::<game::ReceiveGamePacketEvent>()
- .add_message::<config::ReceiveConfigPacketEvent>()
- .add_message::<login::ReceiveLoginPacketEvent>()
- //
- .add_message::<game::AddPlayerEvent>()
- .add_message::<game::RemovePlayerEvent>()
- .add_message::<game::UpdatePlayerEvent>()
- .add_message::<ChatReceivedEvent>()
- .add_message::<game::DeathEvent>()
- .add_message::<game::KeepAliveEvent>()
- .add_message::<game::ResourcePackEvent>()
- .add_message::<game::WorldLoadedEvent>()
- .add_message::<login::ReceiveCustomQueryEvent>();
+ app.add_systems(
+ Update,
+ relative_updates::debug_detect_updates_received_on_local_entities,
+ )
+ .add_observer(game::handle_outgoing_packets_observer)
+ .add_observer(config::handle_outgoing_packets_observer)
+ .add_observer(login::handle_outgoing_packets_observer)
+ .add_systems(Update, death_event_on_0_health)
+ .add_message::<game::ReceiveGamePacketEvent>()
+ .add_message::<config::ReceiveConfigPacketEvent>()
+ .add_message::<login::ReceiveLoginPacketEvent>()
+ //
+ .add_message::<game::AddPlayerEvent>()
+ .add_message::<game::RemovePlayerEvent>()
+ .add_message::<game::UpdatePlayerEvent>()
+ .add_message::<ChatReceivedEvent>()
+ .add_message::<game::DeathEvent>()
+ .add_message::<game::KeepAliveEvent>()
+ .add_message::<game::ResourcePackEvent>()
+ .add_message::<game::WorldLoadedEvent>()
+ .add_message::<login::ReceiveCustomQueryEvent>();
}
}
@@ -70,12 +75,21 @@ macro_rules! __declare_packet_handlers {
pub(crate) use __declare_packet_handlers as declare_packet_handlers;
+#[derive(Resource)]
+struct CachedSystemState<T: SystemParam + 'static>(SystemState<T>);
+
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);
+ // creating a new SystemState is expensive, so we save them as a Resource in the
+ // ecs
+ let mut system_state = match ecs.remove_resource::<CachedSystemState<T>>() {
+ Some(s) => s.0,
+ None => SystemState::<T>::new(ecs),
+ };
let values = system_state.get_mut(ecs);
f(values);
system_state.apply(ecs);
+ ecs.insert_resource(CachedSystemState(system_state));
}
diff --git a/azalea-client/src/plugins/packet/relative_updates.rs b/azalea-client/src/plugins/packet/relative_updates.rs
new file mode 100644
index 00000000..2f7112b8
--- /dev/null
+++ b/azalea-client/src/plugins/packet/relative_updates.rs
@@ -0,0 +1,163 @@
+// How entity updates are processed (to avoid issues with shared worlds)
+// - each bot contains a map of { entity id: updates received }
+// - the shared world also contains a canonical "true" updates received for each
+// entity
+// - when a client loads an entity, its "updates received" is set to the same as
+// the global "updates received"
+// - when the shared world sees an entity for the first time, the "updates
+// received" is set to 1.
+// - clients can force the shared "updates received" to 0 to make it so certain
+// entities (i.e. other bots in our swarm) don't get confused and updated by
+// other bots
+// - when a client gets an update to an entity, we check if our "updates
+// received" is the same as the shared world's "updates received": if it is,
+// then process the update and increment the client's and shared world's
+// "updates received" if not, then we simply increment our local "updates
+// received" and do nothing else
+
+use std::sync::Arc;
+
+use azalea_core::entity_id::MinecraftEntityId;
+use azalea_entity::LocalEntity;
+use azalea_world::PartialWorld;
+use bevy_ecs::prelude::*;
+use derive_more::{Deref, DerefMut};
+use parking_lot::RwLock;
+use tracing::warn;
+
+use crate::packet::as_system;
+
+/// An [`EntityCommand`] that applies a "relative update" to an entity, which
+/// means this update won't be run multiple times by different clients in the
+/// same world.
+///
+/// This is used to avoid a bug where when there's multiple clients in the same
+/// world and an entity sends a relative move packet to all clients, its
+/// position gets desynced since the relative move is applied multiple times.
+///
+/// Don't use this unless you actually got an entity update packet that all
+/// other clients within render distance will get too. You usually don't need
+/// this when the change isn't relative either.
+pub struct RelativeEntityUpdate {
+ pub partial_world: Arc<RwLock<PartialWorld>>,
+ // a function that takes the entity and updates it
+ pub update: Box<dyn FnOnce(&mut EntityWorldMut) + Send + Sync>,
+}
+impl RelativeEntityUpdate {
+ pub fn new(
+ partial_world: Arc<RwLock<PartialWorld>>,
+ update: impl FnOnce(&mut EntityWorldMut) + Send + Sync + 'static,
+ ) -> Self {
+ Self {
+ partial_world,
+ update: Box::new(update),
+ }
+ }
+}
+
+/// A component that counts the number of times this entity has been modified.
+///
+/// This is used for making sure two clients don't do the same relative update
+/// on an entity.
+///
+/// If an entity is local (i.e. it's a client/LocalEntity), this component
+/// should NOT be present in the entity.
+#[derive(Component, Debug, Deref, DerefMut)]
+pub struct UpdatesReceived(u32);
+
+pub type EntityUpdateQuery<'world, 'state, 'a> = Query<
+ 'world,
+ 'state,
+ (
+ &'a MinecraftEntityId,
+ Option<&'a UpdatesReceived>,
+ Option<&'a LocalEntity>,
+ ),
+>;
+
+/// See [`RelativeEntityUpdate`] for details.
+///
+/// Calling this function will have the same effect as using the Command, but
+/// it's more performant than the Command.
+pub fn should_apply_entity_update(
+ commands: &mut Commands,
+ partial_world: &mut PartialWorld,
+ entity: Entity,
+ entity_update_query: EntityUpdateQuery,
+) -> bool {
+ let partial_entity_infos = &mut partial_world.entity_infos;
+
+ if Some(entity) == partial_entity_infos.owner_entity {
+ // if the entity owns this partial world, it's always allowed to update itself
+ return true;
+ };
+
+ let Ok((minecraft_entity_id, updates_received, local_entity)) = entity_update_query.get(entity)
+ else {
+ warn!("called should_apply_entity_update on an entity with missing components");
+ return false;
+ };
+
+ if local_entity.is_some() {
+ // a client tried to update another client, which isn't allowed
+ return false;
+ }
+
+ let this_client_updates_received = partial_entity_infos
+ .updates_received
+ .get(&minecraft_entity_id)
+ .copied();
+
+ let can_update = if let Some(updates_received) = updates_received {
+ this_client_updates_received.unwrap_or(1) == **updates_received
+ } else {
+ // no UpdatesReceived means the entity was just spawned
+ true
+ };
+ if can_update {
+ let new_updates_received = this_client_updates_received.unwrap_or(0) + 1;
+ partial_entity_infos
+ .updates_received
+ .insert(*minecraft_entity_id, new_updates_received);
+
+ commands
+ .entity(entity)
+ .insert(UpdatesReceived(new_updates_received));
+
+ return true;
+ }
+ false
+}
+
+impl EntityCommand for RelativeEntityUpdate {
+ fn apply(self, mut entity_mut: EntityWorldMut) {
+ let partial_world = self.partial_world.clone();
+ let mut should_update = false;
+ let entity = entity_mut.id();
+
+ entity_mut.world_scope(|ecs| {
+ as_system::<(Commands, EntityUpdateQuery)>(ecs, |(mut commands, query)| {
+ should_update = should_apply_entity_update(
+ &mut commands,
+ &mut partial_world.write(),
+ entity,
+ query,
+ );
+ });
+ });
+
+ if should_update {
+ (self.update)(&mut entity_mut);
+ }
+ }
+}
+
+/// A system that logs a warning if an entity has both [`UpdatesReceived`]
+/// and [`LocalEntity`].
+pub fn debug_detect_updates_received_on_local_entities(
+ query: Query<Entity, (With<LocalEntity>, With<UpdatesReceived>)>,
+) {
+ for entity in &query {
+ warn!("Entity {entity:?} has both LocalEntity and UpdatesReceived");
+ }
+}