azalea_entity/plugin/
indexing.rs1use 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 entity_by_uuid: HashMap<Uuid, Entity>,
25}
26
27#[derive(Component, Default)]
34pub struct EntityIdIndex {
35 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#[derive(Component, Debug, Deref, DerefMut)]
89pub struct EntityChunkPos(pub ChunkPos);
90
91pub 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 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
122pub 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#[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 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 commands.entity(entity).despawn();
171
172 continue;
173 };
174
175 let mut instance = instance_lock.write();
176
177 if !loaded_by.is_empty() {
179 continue;
180 }
181
182 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 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 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 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 entity_id_index.insert(entity_id, ecs_entity);
221
222 instance.entity_by_id.insert(entity_id, ecs_entity);
224
225 if let Some(uuid) = entity_uuid {
226 entity_uuid_index.insert(uuid, ecs_entity);
228 }
229}