1
2
Fork 0
mirror of https://github.com/mat-1/azalea.git synced 2025-08-02 06:16:04 +00:00

check for entity duplication before spawning

this fixes behavior where in swarms entities in the world might sometimes have a duplicate that gets spawned and despawned immediately
This commit is contained in:
mat 2023-09-28 21:57:36 -05:00
parent 5977f79400
commit 0bf8291388
7 changed files with 157 additions and 251 deletions

View file

@ -25,7 +25,7 @@ use azalea_buf::McBufWritable;
use azalea_chat::FormattedText;
use azalea_core::{ResourceLocation, Vec3};
use azalea_entity::{
indexing::{EntityIdIndex, Loaded},
indexing::{EntityIdIndex, EntityUuidIndex},
metadata::Health,
EntityPlugin, EntityUpdateSet, EyeHeight, LocalEntity, Position,
};
@ -208,8 +208,6 @@ impl Client {
resolved_address: &SocketAddr,
run_schedule_sender: mpsc::UnboundedSender<()>,
) -> Result<(Self, mpsc::UnboundedReceiver<Event>), JoinError> {
let entity = ecs_lock.lock().spawn(account.to_owned()).id();
let conn = Connection::new(resolved_address).await?;
let (mut conn, game_profile) = Self::handshake(conn, account, address).await?;
@ -236,6 +234,22 @@ impl Client {
let mut ecs = ecs_lock.lock();
// check if an entity with our uuid already exists in the ecs and if so then
// just use that
let entity = {
let entity_uuid_index = ecs.resource::<EntityUuidIndex>();
if let Some(entity) = entity_uuid_index.get(&game_profile.uuid) {
debug!("Reusing entity {entity:?} for client");
entity
} else {
let entity = ecs.spawn_empty().id();
debug!("Created new entity {entity:?} for client");
// add to the uuid index
let mut entity_uuid_index = ecs.resource_mut::<EntityUuidIndex>();
entity_uuid_index.insert(game_profile.uuid, entity);
entity
}
};
// we got the ConfigurationConnection, so the client is now connected :)
let client = Client::new(
game_profile.clone(),
@ -256,6 +270,7 @@ impl Client {
received_registries: ReceivedRegistries::default(),
local_player_events: LocalPlayerEvents(tx),
game_profile: GameProfileComponent(game_profile),
account: account.to_owned(),
},
InConfigurationState,
));
@ -578,6 +593,7 @@ pub struct LocalPlayerBundle {
pub received_registries: ReceivedRegistries,
pub local_player_events: LocalPlayerEvents,
pub game_profile: GameProfileComponent,
pub account: Account,
}
/// A bundle for the components that are present on a local player that is
@ -603,7 +619,6 @@ pub struct JoinedClientBundle {
pub attack: attack::AttackBundle,
pub _local_entity: LocalEntity,
pub _loaded: Loaded,
}
/// A marker component for local players that are currently in the

View file

@ -1,7 +1,7 @@
use std::io::Cursor;
use std::sync::Arc;
use azalea_entity::indexing::{EntityIdIndex, Loaded};
use azalea_entity::indexing::EntityIdIndex;
use azalea_protocol::packets::configuration::serverbound_finish_configuration_packet::ServerboundFinishConfigurationPacket;
use azalea_protocol::packets::configuration::serverbound_keep_alive_packet::ServerboundKeepAlivePacket;
use azalea_protocol::packets::configuration::serverbound_pong_packet::ServerboundPongPacket;
@ -149,7 +149,6 @@ pub fn process_packet_events(ecs: &mut World) {
attack: crate::attack::AttackBundle::default(),
_local_entity: azalea_entity::LocalEntity,
_loaded: Loaded,
});
}
ClientboundConfigurationPacket::KeepAlive(p) => {

View file

@ -194,11 +194,13 @@ pub fn process_packet_events(ecs: &mut World) {
&ClientInformation,
&ReceivedRegistries,
Option<&mut InstanceName>,
Option<&mut LoadedBy>,
&mut EntityIdIndex,
&mut InstanceHolder,
)>,
EventWriter<InstanceLoadedEvent>,
ResMut<InstanceContainer>,
ResMut<EntityUuidIndex>,
EventWriter<SendPacketEvent>,
)> = SystemState::new(ecs);
let (
@ -206,6 +208,7 @@ pub fn process_packet_events(ecs: &mut World) {
mut query,
mut instance_loaded_events,
mut instance_container,
mut entity_uuid_index,
mut send_packet_events,
) = system_state.get_mut(ecs);
let (
@ -213,6 +216,7 @@ pub fn process_packet_events(ecs: &mut World) {
client_information,
received_registries,
instance_name,
loaded_by,
mut entity_id_index,
mut instance_holder,
) = query.get_mut(player_entity).unwrap();
@ -277,9 +281,10 @@ pub fn process_packet_events(ecs: &mut World) {
),
metadata: PlayerMetadataBundle::default(),
};
let entity_id = MinecraftEntityId(p.player_id);
// insert our components into the ecs :)
commands.entity(player_entity).insert((
MinecraftEntityId(p.player_id),
entity_id,
LocalGameMode {
current: p.common.game_type,
previous: p.common.previous_game_type.into(),
@ -289,8 +294,23 @@ pub fn process_packet_events(ecs: &mut World) {
player_bundle,
));
// add our own player to our index
entity_id_index.insert(MinecraftEntityId(p.player_id), player_entity);
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
@ -595,10 +615,7 @@ pub fn process_packet_events(ecs: &mut World) {
if !this_client_has_chunk {
if let Some(shared_chunk) = shared_chunk {
trace!(
"Skipping parsing chunk {:?} because we already know about it",
pos
);
trace!("Skipping parsing chunk {pos:?} because we already know about it");
partial_world.chunks.set_with_shared_reference(
&pos,
Some(shared_chunk.clone()),
@ -608,12 +625,7 @@ pub fn process_packet_events(ecs: &mut World) {
}
}
let heightmaps = p
.chunk_data
.heightmaps
.as_compound()
.and_then(|c| c.get(""))
.and_then(|c| c.as_compound());
let heightmaps = p.chunk_data.heightmaps.as_compound();
// necessary to make the unwrap_or work
let empty_nbt_compound = NbtCompound::default();
let heightmaps = heightmaps.unwrap_or(&empty_nbt_compound);
@ -624,7 +636,7 @@ pub fn process_packet_events(ecs: &mut World) {
heightmaps,
&mut world.chunks,
) {
error!("Couldn't set chunk data: {}", e);
error!("Couldn't set chunk data: {e}");
}
}
ClientboundGamePacket::AddEntity(p) => {
@ -634,50 +646,83 @@ pub fn process_packet_events(ecs: &mut World) {
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, instance_container, mut entity_uuid_index) =
system_state.get_mut(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();
if let Some(instance_name) = instance_name {
let bundle = p.as_entity_bundle((**instance_name).clone());
let mut spawned = commands.spawn((
MinecraftEntityId(p.id),
LoadedBy(HashSet::from([player_entity])),
bundle,
));
entity_id_index.insert(MinecraftEntityId(p.id), spawned.id());
let entity_id = MinecraftEntityId(p.id);
{
// add it to the indexes immediately so if there's a packet that references
// it immediately after it still works
let instance = instance_container.get(instance_name).unwrap();
instance
.write()
.entity_by_id
.insert(MinecraftEntityId(p.id), spawned.id());
entity_uuid_index.insert(p.uuid, spawned.id());
}
if let Some(tab_list) = tab_list {
// technically this makes it possible for non-player entities to have
// GameProfileComponents but the server would have to be doing something
// really weird
if let Some(player_info) = tab_list.get(&p.uuid) {
spawned.insert(GameProfileComponent(player_info.profile.clone()));
}
}
// the bundle doesn't include the default entity metadata so we add that
// separately
p.apply_metadata(&mut spawned);
} else {
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);
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 = spawned.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) => {
@ -901,6 +946,8 @@ pub fn process_packet_events(ecs: &mut World) {
);
continue;
};
// the [`remove_despawned_entities_from_indexes`] system will despawn the entity
// if it's not loaded by anything anymore
loaded_by.remove(&player_entity);
}
}

View file

@ -1,19 +1,19 @@
//! Stuff related to entity indexes and keeping track of entities in the world.
use azalea_core::ChunkPos;
use azalea_world::{InstanceContainer, InstanceName, MinecraftEntityId};
use azalea_world::{Instance, InstanceContainer, InstanceName, MinecraftEntityId};
use bevy_ecs::{
component::Component,
entity::Entity,
query::{Changed, With, Without},
query::Changed,
system::{Commands, Query, Res, ResMut, Resource},
};
use log::{debug, error, info, warn};
use log::{debug, warn};
use nohash_hasher::IntMap;
use std::{collections::HashMap, fmt::Debug};
use uuid::Uuid;
use crate::{EntityUuid, LastSentPosition, LocalEntity, Position};
use crate::{EntityUuid, LastSentPosition, Position};
use super::LoadedBy;
@ -50,6 +50,10 @@ impl EntityUuidIndex {
pub fn insert(&mut self, uuid: Uuid, entity: Entity) {
self.entity_by_uuid.insert(uuid, entity);
}
pub fn remove(&mut self, uuid: &Uuid) -> Option<Entity> {
self.entity_by_uuid.remove(uuid)
}
}
impl EntityIdIndex {
@ -76,180 +80,29 @@ impl Debug for EntityUuidIndex {
}
}
/// A marker component for entities that are in the world and aren't temporary
/// duplicates of other ones. This is meant to be used as a query filter
/// `Added<Loaded>` (since if you do `Added` with another component it might
/// trigger multiple times when in a swarm due to how entities are handled for
/// swarms).
#[derive(Component)]
pub struct Loaded;
// TODO: this should keep track of chunk position changes in a better way
// instead of relying on LastSentPosition
/// Remove new entities that have the same id as an existing entity, and
/// increase the reference counts.
/// Update the chunk position indexes in [`Instance::entities_by_chunk`].
///
/// This is the reason why spawning entities into the ECS when you get a spawn
/// entity packet is okay. This system will make sure the new entity gets
/// combined into the old one.
#[allow(clippy::type_complexity)]
pub fn deduplicate_entities(
mut commands: Commands,
mut query: Query<
(Entity, &MinecraftEntityId, &InstanceName, Option<&Loaded>),
(Changed<MinecraftEntityId>, Without<LocalEntity>),
>,
mut loaded_by_query: Query<&mut LoadedBy>,
mut entity_id_index_query: Query<&mut EntityIdIndex>,
instance_container: Res<InstanceContainer>,
) {
// if this entity already exists, remove it and keep the old one
for (new_entity, id, world_name, loaded) in query.iter_mut() {
let Some(world_lock) = instance_container.get(world_name) else {
error!("Entity was inserted into a world that doesn't exist.");
continue;
};
let world = world_lock.write();
let Some(old_entity) = world.entity_by_id.get(id) else {
// not in index yet, so it's good
if loaded.is_none() {
commands.entity(new_entity).insert(Loaded);
}
continue;
};
if old_entity == &new_entity {
if loaded.is_none() {
commands.entity(new_entity).insert(Loaded);
}
continue;
}
// this entity already exists!!! remove the one we just added but increase
// the reference count
let new_loaded_by = loaded_by_query
.get(new_entity)
.expect("Entities should always have the LoadedBy component ({new_entity:?} did not)")
.clone();
// update the `EntityIdIndex`s of the local players that have this entity loaded
for local_player in new_loaded_by.iter() {
let mut entity_id_index = entity_id_index_query
.get_mut(*local_player)
.expect("Local players should always have the EntityIdIndex component ({local_player:?} did not)");
entity_id_index.insert(*id, *old_entity);
}
let old_loaded_by = loaded_by_query.get_mut(*old_entity);
// merge them if possible
if let Ok(mut old_loaded_by) = old_loaded_by {
old_loaded_by.extend(new_loaded_by.iter());
}
commands.entity(new_entity).despawn();
info!(
"Entity with id {id:?} / {new_entity:?} already existed in the world, merging it with {old_entity:?}"
);
continue;
}
}
// when a local entity is added, if there was already an entity with the same id
// then delete the old entity
#[allow(clippy::type_complexity)]
pub fn deduplicate_local_entities(
mut commands: Commands,
mut query: Query<
(Entity, &MinecraftEntityId, &InstanceName),
(Changed<MinecraftEntityId>, With<LocalEntity>),
>,
instance_container: Res<InstanceContainer>,
) {
// if this entity already exists, remove the old one
for (new_entity, id, world_name) in query.iter_mut() {
let Some(world_lock) = instance_container.get(world_name) else {
error!("Entity was inserted into a world that doesn't exist.");
continue;
};
let world = world_lock.write();
let Some(old_entity) = world.entity_by_id.get(id) else {
continue;
};
if old_entity == &new_entity {
// lol
continue;
}
commands.entity(*old_entity).despawn();
debug!(
"Added local entity {id:?} / {new_entity:?} but already existed in world as {old_entity:?}, despawning {old_entity:?}"
);
break;
}
}
pub fn update_uuid_index(
mut entity_infos: ResMut<EntityUuidIndex>,
query: Query<(Entity, &EntityUuid, Option<&LocalEntity>), Changed<EntityUuid>>,
) {
for (entity, &uuid, local) in query.iter() {
// only add it if it doesn't already exist in
// entity_infos.entity_by_uuid
if local.is_none() {
if let Some(old_entity) = entity_infos.entity_by_uuid.get(&uuid) {
debug!(
"Entity with UUID {uuid:?} already existed in the world, not adding to index (old ecs id: {old_entity:?} / new ecs id: {entity:?})"
);
continue;
}
}
entity_infos.entity_by_uuid.insert(*uuid, entity);
}
}
/// System to keep the entity_by_id index up-to-date.
pub fn update_entity_by_id_index(
mut query: Query<
(
Entity,
&MinecraftEntityId,
&InstanceName,
Option<&LocalEntity>,
),
Changed<MinecraftEntityId>,
>,
instance_container: Res<InstanceContainer>,
) {
for (entity, id, world_name, local) in query.iter_mut() {
let world_lock = instance_container.get(world_name).unwrap();
let mut world = world_lock.write();
if local.is_none() {
if let Some(old_entity) = world.entity_by_id.get(id) {
debug!(
"Entity with ID {id:?} already existed in the world, not adding to index (old ecs id: {old_entity:?} / new ecs id: {entity:?})"
);
continue;
}
}
world.entity_by_id.insert(*id, entity);
debug!("Added {entity:?} to {world_name:?} with {id:?}.");
}
}
/// Update the chunk position indexes in [`EntityUuidIndex`].
/// [`Instance::entities_by_chunk`]: azalea_world::Instance::entities_by_chunk
pub fn update_entity_chunk_positions(
mut query: Query<(Entity, &Position, &mut LastSentPosition, &InstanceName), Changed<Position>>,
mut query: Query<(Entity, &Position, &LastSentPosition, &InstanceName), Changed<Position>>,
instance_container: Res<InstanceContainer>,
) {
for (entity, pos, last_pos, world_name) in query.iter_mut() {
let world_lock = instance_container.get(world_name).unwrap();
let mut world = world_lock.write();
let instance_lock = instance_container.get(world_name).unwrap();
let mut instance = instance_lock.write();
let old_chunk = ChunkPos::from(*last_pos);
let new_chunk = ChunkPos::from(*pos);
if old_chunk != new_chunk {
// move the entity from the old chunk to the new one
if let Some(entities) = world.entities_by_chunk.get_mut(&old_chunk) {
if let Some(entities) = instance.entities_by_chunk.get_mut(&old_chunk) {
entities.remove(&entity);
}
world
instance
.entities_by_chunk
.entry(new_chunk)
.or_default()
@ -262,7 +115,7 @@ pub fn update_entity_chunk_positions(
#[allow(clippy::type_complexity)]
pub fn remove_despawned_entities_from_indexes(
mut commands: Commands,
mut entity_infos: ResMut<EntityUuidIndex>,
mut entity_uuid_index: ResMut<EntityUuidIndex>,
instance_container: Res<InstanceContainer>,
query: Query<
(
@ -282,7 +135,7 @@ pub fn remove_despawned_entities_from_indexes(
debug!(
"Despawned entity {entity:?} because it's in an instance that isn't loaded anymore"
);
if entity_infos.entity_by_uuid.remove(uuid).is_none() {
if entity_uuid_index.entity_by_uuid.remove(uuid).is_none() {
warn!(
"Tried to remove entity {entity:?} from the uuid index but it was not there."
);
@ -317,7 +170,7 @@ pub fn remove_despawned_entities_from_indexes(
debug!("Tried to remove entity {entity:?} from chunk {chunk:?} but the chunk was not found.");
}
// remove it from the uuid index
if entity_infos.entity_by_uuid.remove(uuid).is_none() {
if entity_uuid_index.entity_by_uuid.remove(uuid).is_none() {
warn!("Tried to remove entity {entity:?} from the uuid index but it was not there.");
}
if instance.entity_by_id.remove(minecraft_id).is_none() {
@ -326,6 +179,25 @@ pub fn remove_despawned_entities_from_indexes(
// and now remove the entity from the ecs
commands.entity(entity).despawn();
debug!("Despawned entity {entity:?} because it was not loaded by anything.");
continue;
}
}
pub fn add_entity_to_indexes(
entity_id: MinecraftEntityId,
ecs_entity: Entity,
entity_uuid: Option<Uuid>,
entity_id_index: &mut EntityIdIndex,
entity_uuid_index: &mut EntityUuidIndex,
instance: &mut Instance,
) {
// per-client id index
entity_id_index.insert(entity_id, ecs_entity);
// per-instance id index
instance.entity_by_id.insert(entity_id, ecs_entity);
if let Some(uuid) = entity_uuid {
// per-instance uuid index
entity_uuid_index.insert(uuid, ecs_entity);
}
}

View file

@ -5,7 +5,7 @@ use std::collections::HashSet;
use azalea_core::{BlockPos, ChunkPos, Vec3};
use azalea_world::{InstanceContainer, InstanceName, MinecraftEntityId};
use bevy_app::{App, Plugin, PostUpdate, PreUpdate, Update};
use bevy_app::{App, Plugin, PreUpdate, Update};
use bevy_ecs::prelude::*;
use derive_more::{Deref, DerefMut};
use log::debug;
@ -20,9 +20,6 @@ pub use relative_updates::RelativeEntityUpdate;
/// A Bevy [`SystemSet`] for various types of entity updates.
#[derive(SystemSet, Debug, Hash, Eq, PartialEq, Clone)]
pub enum EntityUpdateSet {
/// Remove ECS entities that refer to an entity that was already in the ECS
/// before.
Deduplicate,
/// Create search indexes for entities.
Index,
/// Remove despawned entities from search indexes.
@ -41,23 +38,10 @@ impl Plugin for EntityPlugin {
PreUpdate,
indexing::remove_despawned_entities_from_indexes.in_set(EntityUpdateSet::Deindex),
)
.add_systems(
PostUpdate,
(
indexing::deduplicate_entities,
indexing::deduplicate_local_entities,
)
.in_set(EntityUpdateSet::Deduplicate),
)
.add_systems(
Update,
(
(
indexing::update_entity_chunk_positions,
indexing::update_uuid_index,
indexing::update_entity_by_id_index,
)
.in_set(EntityUpdateSet::Index),
(indexing::update_entity_chunk_positions).in_set(EntityUpdateSet::Index),
(
relative_updates::debug_detect_updates_received_on_local_entities,
debug_new_entity,

View file

@ -28,14 +28,6 @@ fastnbt = "2.4.4"
default = []
serde = ["dep:serde"]
[profile.release]
lto = true
debug = true
[profile.bench]
lto = true
debug = true
[[bench]]
harness = false
name = "nbt"

View file

@ -27,8 +27,5 @@ parking_lot = "^0.12.1"
thiserror = "1.0.48"
uuid = "1.4.1"
[profile.release]
lto = true
[dev-dependencies]
azalea-client = { path = "../azalea-client" }