azalea_entity/plugin/
indexing.rs

1//! Stuff related to entity indexes and keeping track of entities in the world.
2
3use std::{
4    collections::{HashMap, HashSet},
5    fmt::{self, Debug},
6};
7
8use azalea_core::position::ChunkPos;
9use azalea_world::{Instance, InstanceContainer, InstanceName, MinecraftEntityId};
10use bevy_ecs::prelude::*;
11use derive_more::{Deref, DerefMut};
12use nohash_hasher::IntMap;
13use tracing::{debug, trace, warn};
14use uuid::Uuid;
15
16use super::LoadedBy;
17use crate::{EntityUuid, LocalEntity, Position};
18
19#[derive(Resource, Default)]
20pub struct EntityUuidIndex {
21    /// An index of entities by their UUIDs
22    entity_by_uuid: HashMap<Uuid, Entity>,
23}
24impl EntityUuidIndex {
25    pub fn new() -> Self {
26        Self {
27            entity_by_uuid: HashMap::default(),
28        }
29    }
30
31    pub fn get(&self, uuid: &Uuid) -> Option<Entity> {
32        self.entity_by_uuid.get(uuid).copied()
33    }
34
35    pub fn contains_key(&self, uuid: &Uuid) -> bool {
36        self.entity_by_uuid.contains_key(uuid)
37    }
38
39    pub fn insert(&mut self, uuid: Uuid, entity: Entity) {
40        self.entity_by_uuid.insert(uuid, entity);
41    }
42
43    pub fn remove(&mut self, uuid: &Uuid) -> Option<Entity> {
44        self.entity_by_uuid.remove(uuid)
45    }
46}
47
48/// An index of Minecraft entity IDs to Azalea ECS entities.
49///
50/// This is a `Component` so local players can keep track of entity IDs
51/// independently from the instance.
52///
53/// If you need a per-instance instead of per-client version of this, you can
54/// use [`Instance::entity_by_id`].
55#[derive(Component, Default)]
56pub struct EntityIdIndex {
57    /// An index of entities by their MinecraftEntityId
58    entity_by_id: IntMap<MinecraftEntityId, Entity>,
59    id_by_entity: HashMap<Entity, MinecraftEntityId>,
60}
61
62impl EntityIdIndex {
63    pub fn get_by_minecraft_entity(&self, id: MinecraftEntityId) -> Option<Entity> {
64        self.entity_by_id.get(&id).copied()
65    }
66    pub fn get_by_ecs_entity(&self, entity: Entity) -> Option<MinecraftEntityId> {
67        self.id_by_entity.get(&entity).copied()
68    }
69
70    pub fn contains_minecraft_entity(&self, id: MinecraftEntityId) -> bool {
71        self.entity_by_id.contains_key(&id)
72    }
73    pub fn contains_ecs_entity(&self, id: Entity) -> bool {
74        self.id_by_entity.contains_key(&id)
75    }
76
77    pub fn insert(&mut self, id: MinecraftEntityId, entity: Entity) {
78        self.entity_by_id.insert(id, entity);
79        self.id_by_entity.insert(entity, id);
80        trace!("Inserted {id} -> {entity:?} into a client's EntityIdIndex");
81    }
82
83    pub fn remove_by_minecraft_entity(&mut self, id: MinecraftEntityId) -> Option<Entity> {
84        if let Some(entity) = self.entity_by_id.remove(&id) {
85            trace!(
86                "Removed {id} -> {entity:?} from a client's EntityIdIndex (using EntityIdIndex::remove)"
87            );
88            self.id_by_entity.remove(&entity);
89            Some(entity)
90        } else {
91            trace!(
92                "Failed to remove {id} from a client's EntityIdIndex (using EntityIdIndex::remove)"
93            );
94            None
95        }
96    }
97
98    pub fn remove_by_ecs_entity(&mut self, entity: Entity) -> Option<MinecraftEntityId> {
99        if let Some(id) = self.id_by_entity.remove(&entity) {
100            trace!(
101                "Removed {id} -> {entity:?} from a client's EntityIdIndex (using EntityIdIndex::remove_by_ecs_entity)."
102            );
103            self.entity_by_id.remove(&id);
104            Some(id)
105        } else {
106            // this is expected to happen when despawning entities if it was already
107            // despawned for another reason (like because the client received a
108            // remove_entities packet, or if we're in a shared instance where entity ids are
109            // different for each client)
110            trace!(
111                "Failed to remove {entity:?} from a client's EntityIdIndex (using EntityIdIndex::remove_by_ecs_entity). This may be expected behavior."
112            );
113            None
114        }
115    }
116}
117
118impl Debug for EntityUuidIndex {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        f.debug_struct("EntityUuidIndex").finish()
121    }
122}
123
124/// The chunk position that an entity is currently in.
125#[derive(Component, Debug, Deref, DerefMut)]
126pub struct EntityChunkPos(pub ChunkPos);
127
128/// Update the chunk position indexes in [`Instance::entities_by_chunk`].
129///
130/// [`Instance::entities_by_chunk`]: azalea_world::Instance::entities_by_chunk
131pub fn update_entity_chunk_positions(
132    mut query: Query<(Entity, &Position, &InstanceName, &mut EntityChunkPos), Changed<Position>>,
133    instance_container: Res<InstanceContainer>,
134) {
135    for (entity, pos, instance_name, mut entity_chunk_pos) in query.iter_mut() {
136        let old_chunk = **entity_chunk_pos;
137        let new_chunk = ChunkPos::from(*pos);
138        if old_chunk != new_chunk {
139            **entity_chunk_pos = new_chunk;
140
141            if old_chunk != new_chunk {
142                let Some(instance_lock) = instance_container.get(instance_name) else {
143                    continue;
144                };
145                let mut instance = instance_lock.write();
146
147                // move the entity from the old chunk to the new one
148                if let Some(entities) = instance.entities_by_chunk.get_mut(&old_chunk) {
149                    entities.remove(&entity);
150                }
151                instance
152                    .entities_by_chunk
153                    .entry(new_chunk)
154                    .or_default()
155                    .insert(entity);
156                trace!("Entity {entity:?} moved from {old_chunk:?} to {new_chunk:?}");
157            }
158        }
159    }
160}
161
162/// Insert new entities into [`Instance::entities_by_chunk`].
163pub fn insert_entity_chunk_position(
164    query: Query<(Entity, &Position, &InstanceName), Added<EntityChunkPos>>,
165    instance_container: Res<InstanceContainer>,
166) {
167    for (entity, pos, world_name) in query.iter() {
168        let Some(instance_lock) = instance_container.get(world_name) else {
169            // entity must've been despawned already
170            continue;
171        };
172        let mut instance = instance_lock.write();
173
174        let chunk = ChunkPos::from(*pos);
175        instance
176            .entities_by_chunk
177            .entry(chunk)
178            .or_default()
179            .insert(entity);
180    }
181}
182
183/// Despawn entities that aren't being loaded by anything.
184#[allow(clippy::type_complexity)]
185pub fn remove_despawned_entities_from_indexes(
186    mut commands: Commands,
187    mut entity_uuid_index: ResMut<EntityUuidIndex>,
188    instance_container: Res<InstanceContainer>,
189    query: Query<
190        (
191            Entity,
192            &EntityUuid,
193            &MinecraftEntityId,
194            &Position,
195            &InstanceName,
196            &LoadedBy,
197        ),
198        (Changed<LoadedBy>, Without<LocalEntity>),
199    >,
200    mut entity_id_index_query: Query<&mut EntityIdIndex>,
201) {
202    for (entity, uuid, minecraft_id, position, instance_name, loaded_by) in &query {
203        let Some(instance_lock) = instance_container.get(instance_name) else {
204            // the instance isn't even loaded by us, so we can safely delete the entity
205            debug!(
206                "Despawned entity {entity:?} because it's in an instance that isn't loaded anymore"
207            );
208            if entity_uuid_index.entity_by_uuid.remove(uuid).is_none() {
209                warn!(
210                    "Tried to remove entity {entity:?} from the uuid index but it was not there."
211                );
212            }
213            // and now remove the entity from the ecs
214            commands.entity(entity).despawn();
215
216            continue;
217        };
218
219        let mut instance = instance_lock.write();
220
221        // if the entity is being loaded by any of our clients, don't despawn it
222        if !loaded_by.is_empty() {
223            continue;
224        }
225
226        // remove the entity from the chunk index
227        let chunk = ChunkPos::from(position);
228        match instance.entities_by_chunk.get_mut(&chunk) {
229            Some(entities_in_chunk) => {
230                if entities_in_chunk.remove(&entity) {
231                    // remove the chunk if there's no entities in it anymore
232                    if entities_in_chunk.is_empty() {
233                        instance.entities_by_chunk.remove(&chunk);
234                    }
235                } else {
236                    // search all the other chunks for it :(
237                    let mut found_in_other_chunks = HashSet::new();
238                    for (other_chunk, entities_in_other_chunk) in &mut instance.entities_by_chunk {
239                        if entities_in_other_chunk.remove(&entity) {
240                            found_in_other_chunks.insert(other_chunk);
241                        }
242                    }
243                    if found_in_other_chunks.is_empty() {
244                        warn!(
245                            "Tried to remove entity {entity:?} from chunk {chunk:?} but the entity was not there or in any other chunks."
246                        );
247                    } else {
248                        warn!(
249                            "Tried to remove entity {entity:?} from chunk {chunk:?} but the entity was not there. Found in and removed from other chunk(s): {found_in_other_chunks:?}"
250                        );
251                    }
252                }
253            }
254            _ => {
255                let mut found_in_other_chunks = HashSet::new();
256                for (other_chunk, entities_in_other_chunk) in &mut instance.entities_by_chunk {
257                    if entities_in_other_chunk.remove(&entity) {
258                        found_in_other_chunks.insert(other_chunk);
259                    }
260                }
261                if found_in_other_chunks.is_empty() {
262                    warn!(
263                        "Tried to remove entity {entity:?} from chunk {chunk:?} but the chunk was not found and the entity wasn't in any other chunks."
264                    );
265                } else {
266                    warn!(
267                        "Tried to remove entity {entity:?} from chunk {chunk:?} but the chunk was not found. Entity found in and removed from other chunk(s): {found_in_other_chunks:?}"
268                    );
269                }
270            }
271        }
272        // remove it from the uuid index
273        if entity_uuid_index.entity_by_uuid.remove(uuid).is_none() {
274            warn!("Tried to remove entity {entity:?} from the uuid index but it was not there.");
275        }
276        if instance.entity_by_id.remove(minecraft_id).is_none() {
277            debug!(
278                "Tried to remove entity {entity:?} from the per-instance entity id index but it was not there. This may be expected if you're in a shared instance."
279            );
280        }
281
282        // remove it from every client's EntityIdIndex
283        for mut entity_id_index in entity_id_index_query.iter_mut() {
284            entity_id_index.remove_by_ecs_entity(entity);
285        }
286
287        // and now remove the entity from the ecs
288        commands.entity(entity).despawn();
289        debug!("Despawned entity {entity:?} because it was not loaded by anything.");
290    }
291}
292
293pub fn add_entity_to_indexes(
294    entity_id: MinecraftEntityId,
295    ecs_entity: Entity,
296    entity_uuid: Option<Uuid>,
297    entity_id_index: &mut EntityIdIndex,
298    entity_uuid_index: &mut EntityUuidIndex,
299    instance: &mut Instance,
300) {
301    // per-client id index
302    entity_id_index.insert(entity_id, ecs_entity);
303
304    // per-instance id index
305    instance.entity_by_id.insert(entity_id, ecs_entity);
306
307    if let Some(uuid) = entity_uuid {
308        // per-instance uuid index
309        entity_uuid_index.insert(uuid, ecs_entity);
310    }
311}