azalea_entity/plugin/
indexing.rs

1//! Stuff related to entity indexes and keeping track of entities in the world.
2
3use std::{collections::HashMap, fmt::Debug};
4
5use azalea_core::position::ChunkPos;
6use azalea_world::{Instance, InstanceContainer, InstanceName, MinecraftEntityId};
7use bevy_ecs::{
8    component::Component,
9    entity::Entity,
10    query::{Added, Changed},
11    system::{Commands, Query, Res, ResMut, Resource},
12};
13use derive_more::{Deref, DerefMut};
14use nohash_hasher::IntMap;
15use tracing::{debug, warn};
16use uuid::Uuid;
17
18use super::LoadedBy;
19use crate::{EntityUuid, Position};
20
21#[derive(Resource, Default)]
22pub struct EntityUuidIndex {
23    /// An index of entities by their UUIDs
24    entity_by_uuid: HashMap<Uuid, Entity>,
25}
26
27/// An index of Minecraft entity IDs to Azalea ECS entities. This is a
28/// `Component` so local players can keep track of entity IDs independently from
29/// the instance.
30///
31/// If you need a per-instance instead of per-client version of this, you can
32/// use [`Instance::entity_by_id`].
33#[derive(Component, Default)]
34pub struct EntityIdIndex {
35    /// An index of entities by their MinecraftEntityId
36    entity_by_id: IntMap<MinecraftEntityId, Entity>,
37}
38
39impl EntityUuidIndex {
40    pub fn new() -> Self {
41        Self {
42            entity_by_uuid: HashMap::default(),
43        }
44    }
45
46    pub fn get(&self, uuid: &Uuid) -> Option<Entity> {
47        self.entity_by_uuid.get(uuid).copied()
48    }
49
50    pub fn contains_key(&self, uuid: &Uuid) -> bool {
51        self.entity_by_uuid.contains_key(uuid)
52    }
53
54    pub fn insert(&mut self, uuid: Uuid, entity: Entity) {
55        self.entity_by_uuid.insert(uuid, entity);
56    }
57
58    pub fn remove(&mut self, uuid: &Uuid) -> Option<Entity> {
59        self.entity_by_uuid.remove(uuid)
60    }
61}
62
63impl EntityIdIndex {
64    pub fn get(&self, id: MinecraftEntityId) -> Option<Entity> {
65        self.entity_by_id.get(&id).copied()
66    }
67
68    pub fn contains_key(&self, id: MinecraftEntityId) -> bool {
69        self.entity_by_id.contains_key(&id)
70    }
71
72    pub fn insert(&mut self, id: MinecraftEntityId, entity: Entity) {
73        self.entity_by_id.insert(id, entity);
74    }
75
76    pub fn remove(&mut self, id: MinecraftEntityId) -> Option<Entity> {
77        self.entity_by_id.remove(&id)
78    }
79}
80
81impl Debug for EntityUuidIndex {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        f.debug_struct("EntityUuidIndex").finish()
84    }
85}
86
87/// The chunk position that an entity is currently in.
88#[derive(Component, Debug, Deref, DerefMut)]
89pub struct EntityChunkPos(pub ChunkPos);
90
91/// Update the chunk position indexes in [`Instance::entities_by_chunk`].
92///
93/// [`Instance::entities_by_chunk`]: azalea_world::Instance::entities_by_chunk
94pub fn update_entity_chunk_positions(
95    mut query: Query<(Entity, &Position, &InstanceName, &mut EntityChunkPos), Changed<Position>>,
96    instance_container: Res<InstanceContainer>,
97) {
98    for (entity, pos, world_name, mut entity_chunk_pos) in query.iter_mut() {
99        let instance_lock = instance_container.get(world_name).unwrap();
100        let mut instance = instance_lock.write();
101
102        let old_chunk = **entity_chunk_pos;
103        let new_chunk = ChunkPos::from(*pos);
104        if old_chunk != new_chunk {
105            **entity_chunk_pos = new_chunk;
106
107            if old_chunk != new_chunk {
108                // move the entity from the old chunk to the new one
109                if let Some(entities) = instance.entities_by_chunk.get_mut(&old_chunk) {
110                    entities.remove(&entity);
111                }
112                instance
113                    .entities_by_chunk
114                    .entry(new_chunk)
115                    .or_default()
116                    .insert(entity);
117            }
118        }
119    }
120}
121
122/// Insert new entities into [`Instance::entities_by_chunk`].
123pub fn insert_entity_chunk_position(
124    query: Query<(Entity, &Position, &InstanceName), Added<EntityChunkPos>>,
125    instance_container: Res<InstanceContainer>,
126) {
127    for (entity, pos, world_name) in query.iter() {
128        let instance_lock = instance_container.get(world_name).unwrap();
129        let mut instance = instance_lock.write();
130
131        let chunk = ChunkPos::from(*pos);
132        instance
133            .entities_by_chunk
134            .entry(chunk)
135            .or_default()
136            .insert(entity);
137    }
138}
139
140/// Despawn entities that aren't being loaded by anything.
141#[allow(clippy::type_complexity)]
142pub fn remove_despawned_entities_from_indexes(
143    mut commands: Commands,
144    mut entity_uuid_index: ResMut<EntityUuidIndex>,
145    instance_container: Res<InstanceContainer>,
146    query: Query<
147        (
148            Entity,
149            &EntityUuid,
150            &MinecraftEntityId,
151            &Position,
152            &InstanceName,
153            &LoadedBy,
154        ),
155        Changed<LoadedBy>,
156    >,
157) {
158    for (entity, uuid, minecraft_id, position, world_name, loaded_by) in &query {
159        let Some(instance_lock) = instance_container.get(world_name) else {
160            // the instance isn't even loaded by us, so we can safely delete the entity
161            debug!(
162                "Despawned entity {entity:?} because it's in an instance that isn't loaded anymore"
163            );
164            if entity_uuid_index.entity_by_uuid.remove(uuid).is_none() {
165                warn!(
166                    "Tried to remove entity {entity:?} from the uuid index but it was not there."
167                );
168            }
169            // and now remove the entity from the ecs
170            commands.entity(entity).despawn();
171
172            continue;
173        };
174
175        let mut instance = instance_lock.write();
176
177        // if the entity has no references left, despawn it
178        if !loaded_by.is_empty() {
179            continue;
180        }
181
182        // remove the entity from the chunk index
183        let chunk = ChunkPos::from(*position);
184        if let Some(entities_in_chunk) = instance.entities_by_chunk.get_mut(&chunk) {
185            if entities_in_chunk.remove(&entity) {
186                // remove the chunk if there's no entities in it anymore
187                if entities_in_chunk.is_empty() {
188                    instance.entities_by_chunk.remove(&chunk);
189                }
190            } else {
191                warn!(
192                    "Tried to remove entity {entity:?} from chunk {chunk:?} but the entity was not there."
193                );
194            }
195        } else {
196            debug!("Tried to remove entity {entity:?} from chunk {chunk:?} but the chunk was not found.");
197        }
198        // remove it from the uuid index
199        if entity_uuid_index.entity_by_uuid.remove(uuid).is_none() {
200            warn!("Tried to remove entity {entity:?} from the uuid index but it was not there.");
201        }
202        if instance.entity_by_id.remove(minecraft_id).is_none() {
203            warn!("Tried to remove entity {entity:?} from the id index but it was not there.");
204        }
205        // and now remove the entity from the ecs
206        commands.entity(entity).despawn();
207        debug!("Despawned entity {entity:?} because it was not loaded by anything.");
208    }
209}
210
211pub fn add_entity_to_indexes(
212    entity_id: MinecraftEntityId,
213    ecs_entity: Entity,
214    entity_uuid: Option<Uuid>,
215    entity_id_index: &mut EntityIdIndex,
216    entity_uuid_index: &mut EntityUuidIndex,
217    instance: &mut Instance,
218) {
219    // per-client id index
220    entity_id_index.insert(entity_id, ecs_entity);
221
222    // per-instance id index
223    instance.entity_by_id.insert(entity_id, ecs_entity);
224
225    if let Some(uuid) = entity_uuid {
226        // per-instance uuid index
227        entity_uuid_index.insert(uuid, ecs_entity);
228    }
229}