1use 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 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#[derive(Component, Default)]
56pub struct EntityIdIndex {
57 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 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#[derive(Component, Debug, Deref, DerefMut)]
126pub struct EntityChunkPos(pub ChunkPos);
127
128pub 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 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
162pub 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 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#[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 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 commands.entity(entity).despawn();
215
216 continue;
217 };
218
219 let mut instance = instance_lock.write();
220
221 if !loaded_by.is_empty() {
223 continue;
224 }
225
226 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 if entities_in_chunk.is_empty() {
233 instance.entities_by_chunk.remove(&chunk);
234 }
235 } else {
236 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 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 for mut entity_id_index in entity_id_index_query.iter_mut() {
284 entity_id_index.remove_by_ecs_entity(entity);
285 }
286
287 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 entity_id_index.insert(entity_id, ecs_entity);
303
304 instance.entity_by_id.insert(entity_id, ecs_entity);
306
307 if let Some(uuid) = entity_uuid {
308 entity_uuid_index.insert(uuid, ecs_entity);
310 }
311}