Skip to main content

steel_core/entity/manager/
mod.rs

1//! World-level entity ownership and lookup.
2//!
3//! Steel deliberately uses a simpler loaded/simulated split than vanilla's
4//! entity section manager. The manager owns runtime entity lookup regardless
5//! of chunk load state; chunks are still the persistence boundary, and only
6//! full simulated chunks tick entities.
7
8use std::{collections::BTreeMap, error::Error, fmt, mem, slice, sync::Arc};
9
10use glam::DVec3;
11use rustc_hash::{FxHashMap, FxHashSet};
12use smallvec::SmallVec;
13use steel_registry::vanilla_entities;
14use steel_utils::locks::SyncRwLock;
15use steel_utils::{ChunkPos, PackedSectionPos, SectionPos, WorldAabb};
16use uuid::Uuid;
17
18use super::{
19    Entity, NullEntityCallback, RemovalReason, SharedEntity, snapshot_old_pos_and_rot_for_tick,
20    tick_vehicle_passengers_with_ticked_if,
21};
22
23// Vanilla treats four blocks as the largest ordinary entity search extent.
24const ENTITY_SPATIAL_CELL_SIZE: f64 = 4.0;
25// Center common integer positions inside cells instead of on cell boundaries.
26const ENTITY_SPATIAL_CELL_OFFSET: f64 = ENTITY_SPATIAL_CELL_SIZE / 2.0;
27// Large queries scan occupied cells instead of materializing an enormous cell range.
28const MAX_DIRECT_SPATIAL_CELL_PROBES: i128 = 4_096;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31struct EntitySpatialCell {
32    x: i32,
33    y: i32,
34    z: i32,
35}
36
37impl EntitySpatialCell {
38    fn containing(x: f64, y: f64, z: f64) -> Self {
39        Self {
40            x: ((x + ENTITY_SPATIAL_CELL_OFFSET) / ENTITY_SPATIAL_CELL_SIZE).floor() as i32,
41            y: ((y + ENTITY_SPATIAL_CELL_OFFSET) / ENTITY_SPATIAL_CELL_SIZE).floor() as i32,
42            z: ((z + ENTITY_SPATIAL_CELL_OFFSET) / ENTITY_SPATIAL_CELL_SIZE).floor() as i32,
43        }
44    }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
48struct EntityQueryOrder {
49    section: PackedSectionPos,
50    insertion: u64,
51}
52
53#[derive(Debug, Clone, Copy)]
54struct EntitySpatialCellBounds {
55    minimum: EntitySpatialCell,
56    maximum: EntitySpatialCell,
57}
58
59impl EntitySpatialCellBounds {
60    fn from_aabb(aabb: &WorldAabb) -> Self {
61        Self {
62            minimum: EntitySpatialCell::containing(aabb.min_x(), aabb.min_y(), aabb.min_z()),
63            maximum: EntitySpatialCell::containing(aabb.max_x(), aabb.max_y(), aabb.max_z()),
64        }
65    }
66
67    const fn contains(self, cell: EntitySpatialCell) -> bool {
68        cell.x >= self.minimum.x
69            && cell.x <= self.maximum.x
70            && cell.y >= self.minimum.y
71            && cell.y <= self.maximum.y
72            && cell.z >= self.minimum.z
73            && cell.z <= self.maximum.z
74    }
75
76    fn direct_probe_count(self) -> i128 {
77        let width = i128::from(self.maximum.x) - i128::from(self.minimum.x) + 1;
78        let height = i128::from(self.maximum.y) - i128::from(self.minimum.y) + 1;
79        let depth = i128::from(self.maximum.z) - i128::from(self.minimum.z) + 1;
80        width * height * depth
81    }
82
83    fn cells(self) -> SmallVec<[EntitySpatialCell; 8]> {
84        let mut cells = SmallVec::new();
85        for x in self.minimum.x..=self.maximum.x {
86            for y in self.minimum.y..=self.maximum.y {
87                for z in self.minimum.z..=self.maximum.z {
88                    cells.push(EntitySpatialCell { x, y, z });
89                }
90            }
91        }
92        cells
93    }
94}
95
96/// Error returned when adding an entity to the runtime world fails.
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub enum AddEntityError {
99    /// The entity is in a chunk that is not active in the world entity manager.
100    ChunkNotLoaded {
101        /// Entity network ID.
102        entity_id: i32,
103        /// Chunk containing the entity.
104        chunk: ChunkPos,
105    },
106    /// Another live entity with the same persistent UUID is already registered.
107    DuplicateUuid {
108        /// Entity network ID.
109        entity_id: i32,
110        /// Duplicate persistent UUID.
111        uuid: Uuid,
112    },
113    /// The entity is already removed and cannot be added to the live world.
114    RemovedEntity {
115        /// Entity network ID.
116        entity_id: i32,
117    },
118}
119
120impl fmt::Display for AddEntityError {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        match self {
123            Self::ChunkNotLoaded { entity_id, chunk } => {
124                write!(f, "entity {entity_id} is in non-loaded chunk {chunk:?}")
125            }
126            Self::DuplicateUuid { entity_id, uuid } => {
127                write!(f, "entity {entity_id} has duplicate UUID {uuid}")
128            }
129            Self::RemovedEntity { entity_id } => {
130                write!(f, "entity {entity_id} is already removed")
131            }
132        }
133    }
134}
135
136impl Error for AddEntityError {}
137
138/// Error returned when a live entity move cannot be committed.
139#[derive(Debug, Clone, PartialEq, Eq)]
140pub enum EntityMoveError {
141    /// The entity is no longer managed as live world state.
142    NotLive {
143        /// Entity network ID.
144        entity_id: i32,
145    },
146    /// The entity is deliberately frozen outside live world membership.
147    Inactive {
148        /// Entity network ID.
149        entity_id: i32,
150    },
151    /// The entity tried to move into a chunk outside active world ownership.
152    UnloadedDestination {
153        /// Entity network ID.
154        entity_id: i32,
155        /// Destination chunk.
156        chunk: ChunkPos,
157    },
158}
159
160impl fmt::Display for EntityMoveError {
161    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162        match self {
163            Self::NotLive { entity_id } => {
164                write!(f, "entity {entity_id} is not live in the world")
165            }
166            Self::Inactive { entity_id } => {
167                write!(f, "entity {entity_id} is inactive outside live world state")
168            }
169            Self::UnloadedDestination { entity_id, chunk } => {
170                write!(
171                    f,
172                    "entity {entity_id} cannot move into non-loaded chunk {chunk:?}"
173                )
174            }
175        }
176    }
177}
178
179impl Error for EntityMoveError {}
180
181/// Whether the manager owns persistence for an entity.
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183pub enum EntityOwnership {
184    /// Normal non-player entity owned by the world entity manager.
185    ManagerOwned,
186    /// Entity whose lifetime is owned elsewhere, such as a player.
187    External,
188}
189
190/// Entity visibility for a chunk column.
191///
192/// Mirrors vanilla `Visibility`: hidden chunks keep entity data inactive,
193/// tracked chunks expose entities to lookup/tracking, and ticking chunks also
194/// run manager-owned entity ticks.
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
196pub enum EntityVisibility {
197    /// Not accessible to entity lookup/tracking and not ticking.
198    Hidden,
199    /// Accessible to entity lookup/tracking but not ticking.
200    Tracked,
201    /// Accessible to entity lookup/tracking and ticking.
202    Ticking,
203}
204
205impl EntityVisibility {
206    /// Returns whether entities in this visibility are accessible to queries and tracking.
207    #[must_use]
208    pub const fn is_accessible(self) -> bool {
209        matches!(self, Self::Tracked | Self::Ticking)
210    }
211
212    /// Returns whether entities in this visibility are eligible for ticking.
213    #[must_use]
214    pub const fn is_ticking(self) -> bool {
215        matches!(self, Self::Ticking)
216    }
217}
218
219/// Entity lifecycle changes caused by manager membership or visibility updates.
220#[derive(Default)]
221pub struct EntityLifecycleChanges {
222    /// Entities that became tracked.
223    pub tracking_started: Vec<SharedEntity>,
224    /// Entities that stopped being tracked.
225    pub tracking_stopped: Vec<SharedEntity>,
226    /// Entities that entered the world entity tick list.
227    pub ticking_started: Vec<SharedEntity>,
228    /// Entities that left the world entity tick list.
229    pub ticking_stopped: Vec<SharedEntity>,
230}
231
232impl fmt::Debug for EntityLifecycleChanges {
233    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234        f.debug_struct("EntityLifecycleChanges")
235            .field("tracking_started", &self.tracking_started.len())
236            .field("tracking_stopped", &self.tracking_stopped.len())
237            .field("ticking_started", &self.ticking_started.len())
238            .field("ticking_stopped", &self.ticking_stopped.len())
239            .finish()
240    }
241}
242
243impl EntityLifecycleChanges {
244    fn extend(&mut self, other: Self) {
245        self.tracking_started.extend(other.tracking_started);
246        self.tracking_stopped.extend(other.tracking_stopped);
247        self.ticking_started.extend(other.ticking_started);
248        self.ticking_stopped.extend(other.ticking_stopped);
249    }
250}
251
252/// Section/chunk membership update caused by a committed entity move.
253#[derive(Debug, Clone)]
254pub struct EntityMoveUpdate {
255    /// Entity network ID.
256    pub entity_id: i32,
257    /// Previous section membership.
258    pub old_section: SectionPos,
259    /// New section membership.
260    pub new_section: SectionPos,
261    /// Previous chunk membership.
262    pub old_chunk: ChunkPos,
263    /// New chunk membership.
264    pub new_chunk: ChunkPos,
265    /// Whether the entity was visible to normal world/tracker queries before the move.
266    pub old_accessible: bool,
267    /// Whether the entity is visible to normal world/tracker queries after the move.
268    pub new_accessible: bool,
269    /// Whether the manager-owned entity was in the tick list before the move.
270    pub old_ticking: bool,
271    /// Whether the manager-owned entity is in the tick list after the move.
272    pub new_ticking: bool,
273}
274
275impl EntityMoveUpdate {
276    /// Returns whether the entity changed sections.
277    #[must_use]
278    pub fn section_changed(&self) -> bool {
279        self.old_section != self.new_section
280    }
281
282    /// Returns whether the entity changed chunks.
283    #[must_use]
284    pub fn chunk_changed(&self) -> bool {
285        self.old_chunk != self.new_chunk
286    }
287
288    /// Returns whether the entity crossed an accessibility boundary.
289    #[must_use]
290    pub const fn accessibility_changed(&self) -> bool {
291        self.old_accessible != self.new_accessible
292    }
293
294    /// Returns whether this move made a previously hidden entity accessible.
295    #[must_use]
296    pub const fn became_accessible(&self) -> bool {
297        !self.old_accessible && self.new_accessible
298    }
299
300    /// Returns whether this move made a previously accessible entity hidden.
301    #[must_use]
302    pub const fn became_inaccessible(&self) -> bool {
303        self.old_accessible && !self.new_accessible
304    }
305
306    /// Returns whether this move made a previously non-ticking entity tick.
307    #[must_use]
308    pub const fn became_ticking(&self) -> bool {
309        !self.old_ticking && self.new_ticking
310    }
311
312    /// Returns whether this move made a previously ticking entity stop ticking.
313    #[must_use]
314    pub const fn became_non_ticking(&self) -> bool {
315        self.old_ticking && !self.new_ticking
316    }
317}
318
319/// Saveable entity that could not be persisted by a chunk save pass.
320#[derive(Debug, Clone, PartialEq, Eq)]
321pub struct UnsavedEntityReport {
322    /// Entity network ID.
323    pub entity_id: i32,
324    /// Entity persistent UUID.
325    pub uuid: Uuid,
326    /// Chunk containing the entity.
327    pub chunk: ChunkPos,
328}
329
330/// Entity changes produced when a chunk becomes loaded.
331#[derive(Default)]
332pub struct ChunkEntityLoadResult {
333    /// Retained entities restored to live world membership.
334    pub restored: Vec<SharedEntity>,
335    /// Live entities in this chunk whose tracking became visible again.
336    pub tracking_started: Vec<SharedEntity>,
337    /// Live entities in this chunk whose ticking became active again.
338    pub ticking_started: Vec<SharedEntity>,
339    /// Whether recovery created save-pending entity state for this chunk.
340    pub needs_save: bool,
341}
342
343/// Entity changes produced when a chunk starts unloading.
344#[derive(Default)]
345pub struct ChunkEntityUnloadStart {
346    /// Entities removed from live ownership and retained for chunk recovery.
347    pub retained: Vec<SharedEntity>,
348    /// Entities whose tracker visibility should stop for this chunk transition.
349    pub tracking_stopped: Vec<SharedEntity>,
350    /// Entities whose ticking should stop for this chunk transition.
351    pub ticking_stopped: Vec<SharedEntity>,
352}
353
354#[derive(Clone)]
355struct EntityEntry {
356    entity: SharedEntity,
357    uuid: Uuid,
358    section: SectionPos,
359    chunk: ChunkPos,
360    bounding_box: WorldAabb,
361    spatial_cells: SmallVec<[EntitySpatialCell; 8]>,
362    section_order: u64,
363    ownership: EntityOwnership,
364}
365
366impl EntityEntry {
367    fn new(entity: SharedEntity, ownership: EntityOwnership) -> Self {
368        let section = SectionPos::from_entity_pos(entity.position());
369        let chunk = ChunkPos::new(section.x(), section.z());
370        let bounding_box = entity.bounding_box();
371        Self {
372            uuid: entity.uuid(),
373            entity,
374            section,
375            chunk,
376            bounding_box,
377            spatial_cells: EntitySpatialCellBounds::from_aabb(&bounding_box).cells(),
378            section_order: 0,
379            ownership,
380        }
381    }
382
383    #[must_use]
384    fn should_save(&self) -> bool {
385        self.ownership == EntityOwnership::ManagerOwned
386            && (!self.entity.is_removed()
387                || self
388                    .entity
389                    .removal_reason()
390                    .is_some_and(RemovalReason::should_save))
391            && !self.entity.is_passenger()
392            && !self.entity.has_exactly_one_player_passenger()
393            && self.entity.entity_type().can_serialize
394    }
395
396    fn query_order(&self) -> EntityQueryOrder {
397        EntityQueryOrder {
398            section: PackedSectionPos::from(self.section),
399            insertion: self.section_order,
400        }
401    }
402}
403
404#[derive(Default)]
405struct ManagerState {
406    chunk_visibility: FxHashMap<ChunkPos, EntityVisibility>,
407    live_by_id: FxHashMap<i32, EntityEntry>,
408    live_by_uuid: FxHashMap<Uuid, i32>,
409    accessible_order: OrderedEntityIds,
410    by_section: BTreeMap<PackedSectionPos, OrderedEntityIds>,
411    by_spatial_cell: FxHashMap<EntitySpatialCell, OrderedEntityIds>,
412    by_chunk: FxHashMap<ChunkPos, FxHashSet<i32>>,
413    unloading_by_chunk: FxHashMap<ChunkPos, Vec<EntityEntry>>,
414    save_pending_by_chunk: FxHashMap<ChunkPos, Vec<EntityEntry>>,
415    tick_list: EntityTickList,
416    next_section_order: u64,
417}
418
419#[derive(Default)]
420struct OrderedEntityIds {
421    ids: Vec<i32>,
422}
423
424impl OrderedEntityIds {
425    fn insert(&mut self, entity_id: i32) -> bool {
426        if self.ids.contains(&entity_id) {
427            return false;
428        }
429        self.ids.push(entity_id);
430        true
431    }
432
433    fn remove(&mut self, entity_id: i32) -> bool {
434        let Some(index) = self.ids.iter().position(|id| *id == entity_id) else {
435            return false;
436        };
437        self.ids.remove(index);
438        true
439    }
440
441    fn insert_at(&mut self, index: usize, entity_id: i32) {
442        assert!(!self.ids.contains(&entity_id));
443        self.ids.insert(index, entity_id);
444    }
445
446    const fn is_empty(&self) -> bool {
447        self.ids.is_empty()
448    }
449
450    fn iter(&self) -> impl Iterator<Item = &i32> {
451        self.ids.iter()
452    }
453}
454
455#[derive(Default)]
456struct EntityTickList {
457    active: FxHashMap<i32, SharedEntity>,
458    order: Vec<i32>,
459}
460
461impl EntityTickList {
462    fn add(&mut self, entity: &SharedEntity) -> bool {
463        let entity_id = entity.id();
464        if self.active.insert(entity_id, entity.clone()).is_some() {
465            return false;
466        }
467        self.order.push(entity_id);
468        true
469    }
470
471    fn remove(&mut self, entity_id: i32) -> Option<SharedEntity> {
472        let removed = self.active.remove(&entity_id)?;
473        self.order.retain(|id| *id != entity_id);
474        Some(removed)
475    }
476
477    fn contains(&self, entity_id: i32) -> bool {
478        self.active.contains_key(&entity_id)
479    }
480
481    fn snapshot(&self) -> Vec<SharedEntity> {
482        self.order
483            .iter()
484            .filter_map(|id| self.active.get(id))
485            .cloned()
486            .collect()
487    }
488}
489
490/// Central world entity manager.
491pub struct WorldEntityManager {
492    state: SyncRwLock<ManagerState>,
493}
494
495impl fmt::Debug for WorldEntityManager {
496    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
497        let state = self.state.read();
498        f.debug_struct("WorldEntityManager")
499            .field("chunk_visibility", &state.chunk_visibility.len())
500            .field("live_entities", &state.live_by_id.len())
501            .field("unloading_chunks", &state.unloading_by_chunk.len())
502            .finish()
503    }
504}
505
506impl WorldEntityManager {
507    /// Creates an empty manager.
508    #[must_use]
509    pub fn new() -> Self {
510        Self {
511            state: SyncRwLock::new(ManagerState::default()),
512        }
513    }
514
515    /// Returns whether runtime entity ownership for this chunk is loaded.
516    ///
517    /// This is Vanilla's separate `areEntitiesLoaded` gate used by block-entity
518    /// ticking; it is intentionally not the stricter entity-ticking visibility.
519    #[must_use]
520    pub(crate) fn is_chunk_loaded(&self, pos: ChunkPos) -> bool {
521        self.state.read().chunk_visibility.contains_key(&pos)
522    }
523
524    /// Marks a chunk as loaded and reactivates retained unloading entities.
525    pub fn on_chunk_loaded(&self, pos: ChunkPos) -> ChunkEntityLoadResult {
526        let mut state = self.state.write();
527        state
528            .chunk_visibility
529            .entry(pos)
530            .or_insert(EntityVisibility::Hidden);
531
532        let mut result = ChunkEntityLoadResult::default();
533        if let Some(entries) = state.unloading_by_chunk.remove(&pos) {
534            result.restored.reserve(entries.len());
535            for entry in entries {
536                if entry.entity.is_removed() {
537                    if entry.should_save() {
538                        result.needs_save = true;
539                        state
540                            .save_pending_by_chunk
541                            .entry(pos)
542                            .or_default()
543                            .push(entry);
544                    }
545                    continue;
546                }
547
548                let entity = entry.entity.clone();
549                Self::insert_live_entry(&mut state, entry);
550                let lifecycle = Self::apply_entity_lifecycle_after_insert(&mut state, entity.id());
551                result.tracking_started.extend(lifecycle.tracking_started);
552                result.ticking_started.extend(lifecycle.ticking_started);
553                result.restored.push(entity);
554            }
555        }
556
557        result
558    }
559
560    /// Updates the entity visibility for a chunk column.
561    pub fn update_chunk_visibility(
562        &self,
563        pos: ChunkPos,
564        visibility: EntityVisibility,
565    ) -> EntityLifecycleChanges {
566        let mut state = self.state.write();
567        let previous = state
568            .chunk_visibility
569            .insert(pos, visibility)
570            .unwrap_or(EntityVisibility::Hidden);
571
572        if previous == visibility {
573            return EntityLifecycleChanges::default();
574        }
575
576        Self::apply_chunk_visibility_change(&mut state, pos, previous, visibility)
577    }
578
579    fn push_unique_entity(
580        entity: &SharedEntity,
581        seen: &mut FxHashSet<i32>,
582        entities: &mut Vec<SharedEntity>,
583    ) {
584        if seen.insert(entity.id()) {
585            entities.push(entity.clone());
586        }
587    }
588
589    /// Moves manager-owned root entities in `pos` out of live world membership while
590    /// retaining them for possible chunk recovery.
591    pub fn begin_chunk_unload(&self, pos: ChunkPos) -> ChunkEntityUnloadStart {
592        let mut state = self.state.write();
593        let previous_visibility = state
594            .chunk_visibility
595            .remove(&pos)
596            .unwrap_or(EntityVisibility::Hidden);
597
598        let ids = Self::entity_ids_in_chunk_order(&state, pos);
599
600        let mut result = ChunkEntityUnloadStart::default();
601        let lifecycle = Self::apply_chunk_visibility_change(
602            &mut state,
603            pos,
604            previous_visibility,
605            EntityVisibility::Hidden,
606        );
607        let mut tracking_stopped_ids = lifecycle
608            .tracking_stopped
609            .iter()
610            .map(|entity| entity.id())
611            .collect::<FxHashSet<_>>();
612        result.tracking_stopped = lifecycle.tracking_stopped;
613        result.ticking_stopped = lifecycle.ticking_stopped;
614
615        let mut root_ids = Vec::new();
616        for entity_id in ids {
617            let Some(entry) = state.live_by_id.get(&entity_id) else {
618                continue;
619            };
620            if entry.ownership != EntityOwnership::ManagerOwned {
621                continue;
622            }
623
624            Self::push_unique_entity(
625                &entry.entity,
626                &mut tracking_stopped_ids,
627                &mut result.tracking_stopped,
628            );
629            if !entry.entity.is_passenger() {
630                root_ids.push(entity_id);
631            }
632        }
633
634        let mut retained = Vec::new();
635        let mut visited = FxHashSet::default();
636        for entity_id in root_ids {
637            Self::retain_unloading_entity_tree(
638                &mut state,
639                entity_id,
640                &mut visited,
641                &mut retained,
642                &mut result.retained,
643                &mut tracking_stopped_ids,
644                &mut result.tracking_stopped,
645            );
646        }
647
648        if !retained.is_empty() {
649            state
650                .unloading_by_chunk
651                .entry(pos)
652                .or_default()
653                .extend(retained);
654        }
655
656        result
657    }
658
659    fn retain_unloading_entity_tree(
660        state: &mut ManagerState,
661        entity_id: i32,
662        visited: &mut FxHashSet<i32>,
663        retained: &mut Vec<EntityEntry>,
664        retained_entities: &mut Vec<SharedEntity>,
665        tracking_stopped_ids: &mut FxHashSet<i32>,
666        tracking_stopped: &mut Vec<SharedEntity>,
667    ) {
668        if !visited.insert(entity_id) {
669            return;
670        }
671
672        let Some(entry) = Self::remove_live_entry(state, entity_id) else {
673            return;
674        };
675
676        if entry.ownership != EntityOwnership::ManagerOwned {
677            let restored_id = entry.entity.id();
678            Self::insert_live_entry(state, entry);
679            let entity_to_tick = state.live_by_id.get(&restored_id).and_then(|entry| {
680                let visibility = Self::lifecycle_visibility_for(
681                    entry,
682                    Self::chunk_visibility(state, entry.chunk),
683                );
684                visibility.is_ticking().then(|| entry.entity.clone())
685            });
686            if let Some(entity) = entity_to_tick {
687                state.tick_list.add(&entity);
688            }
689            return;
690        }
691
692        let passengers = entry.entity.passengers();
693        Self::push_unique_entity(&entry.entity, tracking_stopped_ids, tracking_stopped);
694        retained_entities.push(Arc::clone(&entry.entity));
695        retained.push(entry);
696        for passenger in passengers {
697            Self::retain_unloading_entity_tree(
698                state,
699                passenger.id(),
700                visited,
701                retained,
702                retained_entities,
703                tracking_stopped_ids,
704                tracking_stopped,
705            );
706        }
707    }
708
709    /// Finalizes an unloading chunk. Retained entities are detached and dropped.
710    pub fn finalize_chunk_unload(&self, pos: ChunkPos) {
711        let entries = self
712            .state
713            .write()
714            .unloading_by_chunk
715            .remove(&pos)
716            .unwrap_or_default();
717
718        for entry in entries {
719            entry
720                .entity
721                .set_level_callback(Arc::new(NullEntityCallback));
722            entry.entity.set_removed(RemovalReason::UnloadedToChunk);
723        }
724    }
725
726    /// Registers a live runtime entity.
727    ///
728    /// # Panics
729    ///
730    /// Panics if an entity with the same session network ID is already present. Duplicate runtime
731    /// IDs indicate corrupted manager ownership and cannot be recovered without losing identity.
732    pub fn add_live_entity(
733        &self,
734        entity: SharedEntity,
735        ownership: EntityOwnership,
736    ) -> Result<EntityLifecycleChanges, AddEntityError> {
737        let entry = Self::checked_live_entry(entity, ownership)?;
738        let entity_id = entry.entity.id();
739        let mut state = self.state.write();
740        Self::validate_live_entries(&state, slice::from_ref(&entry), ownership, true)?;
741        Self::insert_live_entry(&mut state, entry);
742        Ok(Self::apply_entity_lifecycle_after_insert(
743            &mut state, entity_id,
744        ))
745    }
746
747    /// Adds a related group of live entities atomically.
748    ///
749    /// Use this for persisted vehicle/passenger trees so registration either
750    /// publishes the whole tree or leaves world indexes unchanged.
751    ///
752    /// # Panics
753    ///
754    /// Panics if the entity tree contains the same session network ID more
755    /// than once. Duplicate runtime IDs indicate corrupted ownership.
756    pub fn add_live_entity_tree(
757        &self,
758        entities: &[SharedEntity],
759        ownership: EntityOwnership,
760    ) -> Result<EntityLifecycleChanges, AddEntityError> {
761        let mut entries = Vec::with_capacity(entities.len());
762        for entity in entities {
763            entries.push(Self::checked_live_entry(Arc::clone(entity), ownership)?);
764        }
765
766        let mut seen_ids = FxHashSet::default();
767        let mut seen_uuids = FxHashSet::default();
768        for entry in &entries {
769            let entity_id = entry.entity.id();
770            assert!(
771                seen_ids.insert(entity_id),
772                "entity id {entity_id} appears more than once in a live entity tree"
773            );
774            if !seen_uuids.insert(entry.uuid) {
775                return Err(AddEntityError::DuplicateUuid {
776                    entity_id,
777                    uuid: entry.uuid,
778                });
779            }
780        }
781
782        let mut state = self.state.write();
783        Self::validate_live_entries(&state, &entries, ownership, false)?;
784        let entity_ids = entries
785            .iter()
786            .map(|entry| entry.entity.id())
787            .collect::<Vec<_>>();
788        for entry in entries {
789            Self::insert_live_entry(&mut state, entry);
790        }
791        let mut lifecycle = EntityLifecycleChanges::default();
792        for entity_id in entity_ids {
793            lifecycle.extend(Self::apply_entity_lifecycle_after_insert(
794                &mut state, entity_id,
795            ));
796        }
797        Ok(lifecycle)
798    }
799
800    fn checked_live_entry(
801        entity: SharedEntity,
802        ownership: EntityOwnership,
803    ) -> Result<EntityEntry, AddEntityError> {
804        if entity.is_removed() {
805            return Err(AddEntityError::RemovedEntity {
806                entity_id: entity.id(),
807            });
808        }
809
810        Ok(EntityEntry::new(entity, ownership))
811    }
812
813    fn validate_live_entries(
814        state: &ManagerState,
815        entries: &[EntityEntry],
816        ownership: EntityOwnership,
817        require_loaded_chunks: bool,
818    ) -> Result<(), AddEntityError> {
819        for entry in entries {
820            let entity_id = entry.entity.id();
821            assert!(
822                !Self::contains_id(state, entity_id),
823                "entity id {entity_id} is already registered in the world entity manager"
824            );
825            if Self::contains_uuid(state, entry.uuid) {
826                return Err(AddEntityError::DuplicateUuid {
827                    entity_id,
828                    uuid: entry.uuid,
829                });
830            }
831            if require_loaded_chunks
832                && ownership == EntityOwnership::ManagerOwned
833                && !state.chunk_visibility.contains_key(&entry.chunk)
834            {
835                return Err(AddEntityError::ChunkNotLoaded {
836                    entity_id,
837                    chunk: entry.chunk,
838                });
839            }
840        }
841        Ok(())
842    }
843
844    /// Removes a live entity for an explicit entity removal reason.
845    pub fn remove_live_entity(
846        &self,
847        entity_id: i32,
848        reason: RemovalReason,
849    ) -> Option<SharedEntity> {
850        let mut state = self.state.write();
851        let entry = Self::remove_live_entry(&mut state, entity_id)?;
852        let entity = entry.entity.clone();
853
854        if reason.should_save() && entry.should_save() {
855            state
856                .save_pending_by_chunk
857                .entry(entry.chunk)
858                .or_default()
859                .push(entry);
860        }
861
862        Some(entity)
863    }
864
865    /// Acknowledges that selected save-pending entities for `chunk` were persisted.
866    pub fn on_chunk_saved(&self, chunk: ChunkPos, saved_entity_ids: &[i32]) {
867        if saved_entity_ids.is_empty() {
868            return;
869        }
870
871        let saved_entity_ids = saved_entity_ids.iter().copied().collect::<FxHashSet<_>>();
872        let mut state = self.state.write();
873        let Some(entries) = state.save_pending_by_chunk.get_mut(&chunk) else {
874            return;
875        };
876
877        entries.retain(|entry| !saved_entity_ids.contains(&entry.entity.id()));
878        if entries.is_empty() {
879            state.save_pending_by_chunk.remove(&chunk);
880        }
881    }
882
883    /// Returns whether `chunk` has removed runtime entities waiting for a save acknowledgement.
884    #[must_use]
885    pub fn has_save_pending_for_chunk(&self, chunk: ChunkPos) -> bool {
886        self.state
887            .read()
888            .save_pending_by_chunk
889            .get(&chunk)
890            .is_some_and(|entries| !entries.is_empty())
891    }
892
893    /// Validates that a live entity can move to `new_pos`.
894    pub fn validate_move(&self, entity_id: i32, new_pos: DVec3) -> Result<(), EntityMoveError> {
895        let state = self.state.read();
896        let Some(entry) = state.live_by_id.get(&entity_id) else {
897            return Err(EntityMoveError::NotLive { entity_id });
898        };
899
900        if entry.ownership == EntityOwnership::ManagerOwned {
901            let new_section = SectionPos::from_entity_pos(new_pos);
902            let new_chunk = ChunkPos::new(new_section.x(), new_section.z());
903            if !Self::can_move_manager_owned_to_chunk(&state, entry, new_chunk) {
904                return Err(EntityMoveError::UnloadedDestination {
905                    entity_id,
906                    chunk: new_chunk,
907                });
908            }
909        }
910
911        Ok(())
912    }
913
914    /// Commits manager indexes after a live entity position change.
915    #[expect(
916        clippy::too_many_lines,
917        reason = "keeps movement index updates atomic under one manager write lock"
918    )]
919    pub fn commit_move(
920        &self,
921        entity_id: i32,
922        new_pos: DVec3,
923    ) -> Result<EntityMoveUpdate, EntityMoveError> {
924        let mut state = self.state.write();
925        let Some(current) = state.live_by_id.get(&entity_id) else {
926            return Err(EntityMoveError::NotLive { entity_id });
927        };
928
929        let new_section = SectionPos::from_entity_pos(new_pos);
930        let new_chunk = ChunkPos::new(new_section.x(), new_section.z());
931        if current.ownership == EntityOwnership::ManagerOwned
932            && !Self::can_move_manager_owned_to_chunk(&state, current, new_chunk)
933        {
934            return Err(EntityMoveError::UnloadedDestination {
935                entity_id,
936                chunk: new_chunk,
937            });
938        }
939
940        let old_section = current.section;
941        let old_chunk = current.chunk;
942        let old_accessible = Self::is_accessible(&state, current);
943        let new_accessible = Self::is_accessible_at(&state, current.ownership, new_chunk);
944        let old_visibility =
945            Self::lifecycle_visibility_for(current, Self::chunk_visibility(&state, old_chunk));
946        let new_visibility =
947            Self::lifecycle_visibility_for(current, Self::chunk_visibility(&state, new_chunk));
948        let old_ticking = old_visibility.is_ticking();
949        let new_ticking = new_visibility.is_ticking();
950        let entity = Arc::clone(&current.entity);
951        let new_bounding_box = entity.bounding_box();
952        let new_spatial_cells = EntitySpatialCellBounds::from_aabb(&new_bounding_box).cells();
953        let spatial_cells_changed = current.spatial_cells != new_spatial_cells;
954        let section_changed = old_section != new_section;
955        let spatial_index_changed = spatial_cells_changed || section_changed;
956        let current_section_order = current.section_order;
957
958        if !section_changed && !spatial_cells_changed {
959            if let Some(entry) = state.live_by_id.get_mut(&entity_id) {
960                entry.bounding_box = new_bounding_box;
961            }
962            return Ok(EntityMoveUpdate {
963                entity_id,
964                old_section,
965                new_section,
966                old_chunk,
967                new_chunk,
968                old_accessible,
969                new_accessible,
970                old_ticking,
971                new_ticking,
972            });
973        }
974
975        if section_changed {
976            Self::remove_from_section(&mut state, old_section, entity_id);
977            Self::remove_from_chunk(&mut state, old_chunk, entity_id);
978        }
979
980        let new_section_order = if section_changed {
981            Self::next_section_order(&mut state)
982        } else {
983            current_section_order
984        };
985        let new_query_order = EntityQueryOrder {
986            section: PackedSectionPos::from(new_section),
987            insertion: new_section_order,
988        };
989
990        if spatial_index_changed {
991            let Some(entry) = state.live_by_id.get_mut(&entity_id) else {
992                return Err(EntityMoveError::NotLive { entity_id });
993            };
994            let previous_cells = mem::take(&mut entry.spatial_cells);
995            Self::remove_from_spatial_cells(&mut state, &previous_cells, entity_id);
996            Self::insert_into_spatial_cells(
997                &mut state,
998                &new_spatial_cells,
999                entity_id,
1000                new_query_order,
1001            );
1002        }
1003
1004        if let Some(entry) = state.live_by_id.get_mut(&entity_id) {
1005            entry.section = new_section;
1006            entry.chunk = new_chunk;
1007            entry.bounding_box = new_bounding_box;
1008            if spatial_index_changed {
1009                entry.spatial_cells = new_spatial_cells;
1010            }
1011            if section_changed {
1012                entry.section_order = new_section_order;
1013            }
1014        }
1015
1016        if section_changed {
1017            state
1018                .by_section
1019                .entry(PackedSectionPos::from(new_section))
1020                .or_default()
1021                .insert(entity_id);
1022            state
1023                .by_chunk
1024                .entry(new_chunk)
1025                .or_default()
1026                .insert(entity_id);
1027        }
1028
1029        if old_accessible && !new_accessible {
1030            state.accessible_order.remove(entity_id);
1031        } else if !old_accessible && new_accessible {
1032            state.accessible_order.insert(entity_id);
1033        }
1034
1035        if old_ticking && !new_ticking {
1036            state.tick_list.remove(entity_id);
1037        } else if !old_ticking && new_ticking {
1038            state.tick_list.add(&entity);
1039        }
1040
1041        Ok(EntityMoveUpdate {
1042            entity_id,
1043            old_section,
1044            new_section,
1045            old_chunk,
1046            new_chunk,
1047            old_accessible,
1048            new_accessible,
1049            old_ticking,
1050            new_ticking,
1051        })
1052    }
1053
1054    /// Commits a live entity bounding-box change to the spatial query index.
1055    ///
1056    /// Reads the current bounds after acquiring the manager lock so concurrent
1057    /// callbacks may complete in either order without restoring stale bounds.
1058    pub fn commit_bounding_box_change(&self, entity_id: i32) {
1059        let mut state = self.state.write();
1060        let Some(current) = state.live_by_id.get(&entity_id) else {
1061            return;
1062        };
1063        let bounding_box = current.entity.bounding_box();
1064        let new_spatial_cells = EntitySpatialCellBounds::from_aabb(&bounding_box).cells();
1065        let query_order = current.query_order();
1066
1067        if current.spatial_cells == new_spatial_cells {
1068            if let Some(entry) = state.live_by_id.get_mut(&entity_id) {
1069                entry.bounding_box = bounding_box;
1070            }
1071            return;
1072        }
1073
1074        let Some(entry) = state.live_by_id.get_mut(&entity_id) else {
1075            return;
1076        };
1077        let previous_cells = mem::take(&mut entry.spatial_cells);
1078        Self::remove_from_spatial_cells(&mut state, &previous_cells, entity_id);
1079        Self::insert_into_spatial_cells(&mut state, &new_spatial_cells, entity_id, query_order);
1080        if let Some(entry) = state.live_by_id.get_mut(&entity_id) {
1081            entry.bounding_box = bounding_box;
1082            entry.spatial_cells = new_spatial_cells;
1083        }
1084    }
1085
1086    fn can_move_manager_owned_to_chunk(
1087        state: &ManagerState,
1088        entry: &EntityEntry,
1089        new_chunk: ChunkPos,
1090    ) -> bool {
1091        state.chunk_visibility.contains_key(&new_chunk)
1092            || (entry.entity.is_passenger()
1093                && Self::has_live_loaded_root_vehicle(state, &entry.entity))
1094    }
1095
1096    fn has_live_loaded_root_vehicle(state: &ManagerState, entity: &SharedEntity) -> bool {
1097        let mut visited = FxHashSet::default();
1098        visited.insert(entity.id());
1099
1100        let mut passenger = Arc::clone(entity);
1101        let Some(mut vehicle) = passenger.vehicle() else {
1102            return false;
1103        };
1104
1105        loop {
1106            assert!(
1107                visited.insert(vehicle.id()),
1108                "cyclic passenger relationship involving entity {}",
1109                entity.id()
1110            );
1111            if vehicle.is_removed() || !vehicle.has_passenger(passenger.as_ref()) {
1112                return false;
1113            }
1114
1115            let Some(vehicle_entry) = state.live_by_id.get(&vehicle.id()) else {
1116                return false;
1117            };
1118
1119            let Some(next_vehicle) = vehicle.vehicle() else {
1120                return match vehicle_entry.ownership {
1121                    EntityOwnership::External => true,
1122                    EntityOwnership::ManagerOwned => {
1123                        state.chunk_visibility.contains_key(&vehicle_entry.chunk)
1124                    }
1125                };
1126            };
1127
1128            passenger = vehicle;
1129            vehicle = next_vehicle;
1130        }
1131    }
1132
1133    #[must_use]
1134    /// Gets a live entity by session network ID.
1135    pub fn get_by_id(&self, entity_id: i32) -> Option<SharedEntity> {
1136        self.state
1137            .read()
1138            .live_by_id
1139            .get(&entity_id)
1140            .map(|entry| entry.entity.clone())
1141    }
1142
1143    /// Returns true if this exact entity is live or retained for chunk-unload recovery.
1144    pub fn contains_live_or_unloading_entity(&self, entity: &SharedEntity) -> bool {
1145        let state = self.state.read();
1146        state
1147            .live_by_id
1148            .get(&entity.id())
1149            .is_some_and(|entry| Arc::ptr_eq(&entry.entity, entity))
1150            || state
1151                .unloading_by_chunk
1152                .values()
1153                .flatten()
1154                .any(|entry| Arc::ptr_eq(&entry.entity, entity))
1155    }
1156
1157    #[must_use]
1158    /// Gets a live entity by session network ID if it is visible to vanilla gameplay lookups.
1159    pub fn get_accessible_by_id(&self, entity_id: i32) -> Option<SharedEntity> {
1160        let state = self.state.read();
1161        let entry = state.live_by_id.get(&entity_id)?;
1162        Self::is_accessible(&state, entry).then(|| entry.entity.clone())
1163    }
1164
1165    #[must_use]
1166    /// Gets a live entity by persistent UUID.
1167    pub fn get_by_uuid(&self, uuid: &Uuid) -> Option<SharedEntity> {
1168        let state = self.state.read();
1169        let entity_id = state.live_by_uuid.get(uuid)?;
1170        state
1171            .live_by_id
1172            .get(entity_id)
1173            .map(|entry| entry.entity.clone())
1174    }
1175
1176    #[must_use]
1177    /// Gets live entities whose bounding boxes intersect `aabb` and match `predicate`.
1178    pub fn get_entities_in_aabb_matching(
1179        &self,
1180        aabb: &WorldAabb,
1181        mut predicate: impl FnMut(&dyn Entity) -> bool,
1182    ) -> Vec<SharedEntity> {
1183        self.get_entities_in_aabb(aabb)
1184            .into_iter()
1185            .filter(|entity| predicate(entity.as_ref()))
1186            .collect()
1187    }
1188
1189    /// Returns whether any live entity intersects `aabb` and matches `predicate`.
1190    #[must_use]
1191    pub fn has_entity_in_aabb_matching(
1192        &self,
1193        aabb: &WorldAabb,
1194        mut predicate: impl FnMut(&dyn Entity) -> bool,
1195    ) -> bool {
1196        let state = self.state.read();
1197        for entry in Self::entity_query_entries(&state, aabb) {
1198            if Self::is_accessible(&state, entry)
1199                && entry.bounding_box.intersects(*aabb)
1200                && predicate(entry.entity.as_ref())
1201            {
1202                return true;
1203            }
1204        }
1205
1206        false
1207    }
1208
1209    /// Gets matching live entity bounding boxes that intersect `aabb`.
1210    #[must_use]
1211    pub fn get_entity_bounding_boxes_in_aabb_matching(
1212        &self,
1213        aabb: &WorldAabb,
1214        mut predicate: impl FnMut(&dyn Entity) -> bool,
1215    ) -> Vec<WorldAabb> {
1216        let state = self.state.read();
1217        let mut result = Vec::new();
1218        for entry in Self::entity_query_entries(&state, aabb) {
1219            if Self::is_accessible(&state, entry)
1220                && entry.bounding_box.intersects(*aabb)
1221                && predicate(entry.entity.as_ref())
1222            {
1223                result.push(entry.bounding_box);
1224            }
1225        }
1226
1227        result
1228    }
1229
1230    #[must_use]
1231    /// Gets the nearest live entity whose bounding box intersects `aabb` and matches `predicate`.
1232    pub fn nearest_entity_in_aabb_matching(
1233        &self,
1234        aabb: &WorldAabb,
1235        origin: DVec3,
1236        mut predicate: impl FnMut(&dyn Entity) -> bool,
1237    ) -> Option<SharedEntity> {
1238        self.get_entities_in_aabb(aabb)
1239            .into_iter()
1240            .filter(|entity| predicate(entity.as_ref()))
1241            .min_by(|first, second| {
1242                first
1243                    .position()
1244                    .distance_squared(origin)
1245                    .total_cmp(&second.position().distance_squared(origin))
1246            })
1247    }
1248
1249    #[must_use]
1250    /// Gets live entities whose bounding boxes intersect `aabb`.
1251    pub fn get_entities_in_aabb(&self, aabb: &WorldAabb) -> Vec<SharedEntity> {
1252        let state = self.state.read();
1253        Self::entity_query_entries(&state, aabb)
1254            .into_iter()
1255            .filter(|entry| {
1256                Self::is_accessible(&state, entry) && entry.bounding_box.intersects(*aabb)
1257            })
1258            .map(|entry| Arc::clone(&entry.entity))
1259            .collect()
1260    }
1261
1262    /// Gets all live entities visible to vanilla gameplay lookups.
1263    #[must_use]
1264    pub fn get_accessible_entities(&self) -> Vec<SharedEntity> {
1265        let state = self.state.read();
1266        state
1267            .accessible_order
1268            .iter()
1269            .filter_map(|entity_id| state.live_by_id.get(entity_id))
1270            .filter(|entry| Self::is_accessible(&state, entry))
1271            .map(|entry| Arc::clone(&entry.entity))
1272            .collect()
1273    }
1274
1275    fn entity_query_entries<'a>(state: &'a ManagerState, aabb: &WorldAabb) -> Vec<&'a EntityEntry> {
1276        let bounds = EntitySpatialCellBounds::from_aabb(aabb);
1277        let mut populated_cells = SmallVec::<[&OrderedEntityIds; 8]>::new();
1278
1279        if bounds.direct_probe_count() <= MAX_DIRECT_SPATIAL_CELL_PROBES {
1280            for x in bounds.minimum.x..=bounds.maximum.x {
1281                for y in bounds.minimum.y..=bounds.maximum.y {
1282                    for z in bounds.minimum.z..=bounds.maximum.z {
1283                        if let Some(cell_ids) =
1284                            state.by_spatial_cell.get(&EntitySpatialCell { x, y, z })
1285                        {
1286                            populated_cells.push(cell_ids);
1287                        }
1288                    }
1289                }
1290            }
1291        } else {
1292            for (cell, cell_ids) in &state.by_spatial_cell {
1293                if bounds.contains(*cell) {
1294                    populated_cells.push(cell_ids);
1295                }
1296            }
1297        }
1298
1299        if let [cell] = populated_cells.as_slice() {
1300            return cell
1301                .iter()
1302                .filter_map(|entity_id| state.live_by_id.get(entity_id))
1303                .collect();
1304        }
1305
1306        let mut entity_ids = FxHashSet::default();
1307        for cell in populated_cells {
1308            entity_ids.extend(cell.iter().copied());
1309        }
1310        let mut entries = entity_ids
1311            .into_iter()
1312            .filter_map(|entity_id| state.live_by_id.get(&entity_id))
1313            .collect::<Vec<_>>();
1314        entries.sort_unstable_by_key(|entry| entry.query_order());
1315        entries
1316    }
1317
1318    fn entity_ids_in_chunk_order(state: &ManagerState, chunk: ChunkPos) -> Vec<i32> {
1319        let first = PackedSectionPos::from(SectionPos::new(chunk.0.x, 0, chunk.0.y));
1320        let last = PackedSectionPos::from(SectionPos::new(chunk.0.x, -1, chunk.0.y));
1321        state
1322            .by_section
1323            .range(first..=last)
1324            .flat_map(|(_, entity_ids)| entity_ids.iter().copied())
1325            .collect()
1326    }
1327
1328    /// Reports saveable entities whose chunks were not part of a chunk save pass.
1329    #[must_use]
1330    pub fn saveable_entities_outside_chunks(
1331        &self,
1332        saved_chunks: &[ChunkPos],
1333    ) -> Vec<UnsavedEntityReport> {
1334        let saved_chunks = saved_chunks.iter().copied().collect::<FxHashSet<_>>();
1335        let state = self.state.read();
1336        let mut seen = FxHashSet::default();
1337        let mut reports = Vec::new();
1338
1339        for entry in state.live_by_id.values() {
1340            Self::push_unsaved_entity_report(&saved_chunks, &mut seen, &mut reports, entry);
1341        }
1342
1343        for entries in state.unloading_by_chunk.values() {
1344            for entry in entries {
1345                Self::push_unsaved_entity_report(&saved_chunks, &mut seen, &mut reports, entry);
1346            }
1347        }
1348
1349        for entries in state.save_pending_by_chunk.values() {
1350            for entry in entries {
1351                Self::push_unsaved_entity_report(&saved_chunks, &mut seen, &mut reports, entry);
1352            }
1353        }
1354
1355        reports.sort_by_key(|report| (report.chunk.0.x, report.chunk.0.y, report.entity_id));
1356        reports
1357    }
1358
1359    #[must_use]
1360    /// Gets entities that should be serialized for `chunk`.
1361    pub fn get_saveable_entities_for_chunk(&self, chunk: ChunkPos) -> Vec<SharedEntity> {
1362        let state = self.state.read();
1363        let mut result = Vec::new();
1364        let mut seen_ids = FxHashSet::default();
1365        let mut seen_uuids = FxHashSet::default();
1366
1367        for entity_id in Self::entity_ids_in_chunk_order(&state, chunk) {
1368            let Some(entry) = state.live_by_id.get(&entity_id) else {
1369                continue;
1370            };
1371            Self::push_saveable_entity(&mut result, &mut seen_ids, &mut seen_uuids, entry);
1372        }
1373
1374        if let Some(entries) = state.unloading_by_chunk.get(&chunk) {
1375            for entry in entries {
1376                Self::push_saveable_entity(&mut result, &mut seen_ids, &mut seen_uuids, entry);
1377            }
1378        }
1379
1380        if let Some(entries) = state.save_pending_by_chunk.get(&chunk) {
1381            for entry in entries {
1382                Self::push_saveable_entity(&mut result, &mut seen_ids, &mut seen_uuids, entry);
1383            }
1384        }
1385
1386        result
1387    }
1388
1389    #[must_use]
1390    /// Gets live entities currently indexed in `chunk`.
1391    pub fn live_entities_in_chunk(&self, chunk: ChunkPos) -> Vec<SharedEntity> {
1392        let state = self.state.read();
1393        Self::entity_ids_in_chunk_order(&state, chunk)
1394            .into_iter()
1395            .filter_map(|entity_id| state.live_by_id.get(&entity_id))
1396            .map(|entry| entry.entity.clone())
1397            .collect()
1398    }
1399
1400    #[must_use]
1401    /// Returns the number of live indexed entities.
1402    pub fn count(&self) -> usize {
1403        self.state.read().live_by_id.len()
1404    }
1405
1406    /// Ticks live entities currently in the ticking visibility set.
1407    pub fn tick_entities(&self, _tick_count: i32, runs_normally: bool) -> FxHashSet<ChunkPos> {
1408        let mut dirty_chunks = FxHashSet::default();
1409        let mut ticked_entities = FxHashSet::default();
1410        let tick_candidates = self.ticking_entities_snapshot();
1411        for entity in tick_candidates {
1412            if !self.can_tick_entity_now(entity.id()) {
1413                continue;
1414            }
1415
1416            if entity.is_removed() {
1417                continue;
1418            }
1419
1420            if Self::is_entity_frozen_by_tick_rate(entity.as_ref(), runs_normally) {
1421                continue;
1422            }
1423
1424            let entity_chunk = self.live_manager_owned_entity_chunk(entity.id());
1425            entity.check_despawn();
1426            if entity.is_removed() {
1427                if let Some(chunk) = entity_chunk {
1428                    dirty_chunks.insert(chunk);
1429                }
1430                continue;
1431            }
1432
1433            if Self::is_valid_passenger_or_stop_riding(&entity) {
1434                continue;
1435            }
1436
1437            if !ticked_entities.insert(entity.id()) {
1438                continue;
1439            }
1440
1441            self.tick_non_passenger(&entity, &mut ticked_entities, &mut dirty_chunks);
1442        }
1443        dirty_chunks
1444    }
1445
1446    fn ticking_entities_snapshot(&self) -> Vec<SharedEntity> {
1447        self.state.read().tick_list.snapshot()
1448    }
1449
1450    fn live_manager_owned_entity_chunk(&self, entity_id: i32) -> Option<ChunkPos> {
1451        self.state
1452            .read()
1453            .live_by_id
1454            .get(&entity_id)
1455            .filter(|entry| entry.ownership == EntityOwnership::ManagerOwned)
1456            .map(|entry| entry.chunk)
1457    }
1458
1459    fn chunk_visibility(state: &ManagerState, chunk: ChunkPos) -> EntityVisibility {
1460        state
1461            .chunk_visibility
1462            .get(&chunk)
1463            .copied()
1464            .unwrap_or(EntityVisibility::Hidden)
1465    }
1466
1467    fn effective_visibility(
1468        entry: &EntityEntry,
1469        chunk_visibility: EntityVisibility,
1470    ) -> EntityVisibility {
1471        if entry.entity.is_always_ticking() {
1472            return EntityVisibility::Ticking;
1473        }
1474        if entry.ownership == EntityOwnership::External {
1475            return EntityVisibility::Tracked;
1476        }
1477        chunk_visibility
1478    }
1479
1480    fn lifecycle_visibility_for(
1481        entry: &EntityEntry,
1482        chunk_visibility: EntityVisibility,
1483    ) -> EntityVisibility {
1484        Self::effective_visibility(entry, chunk_visibility)
1485    }
1486
1487    fn apply_entity_lifecycle_after_insert(
1488        state: &mut ManagerState,
1489        entity_id: i32,
1490    ) -> EntityLifecycleChanges {
1491        let Some(entry) = state.live_by_id.get(&entity_id) else {
1492            return EntityLifecycleChanges::default();
1493        };
1494        let visibility =
1495            Self::lifecycle_visibility_for(entry, Self::chunk_visibility(state, entry.chunk));
1496        let entity = entry.entity.clone();
1497        let should_tick = visibility.is_ticking();
1498
1499        let mut lifecycle = EntityLifecycleChanges::default();
1500        if visibility.is_accessible() {
1501            lifecycle.tracking_started.push(entity.clone());
1502        }
1503        if should_tick && state.tick_list.add(&entity) {
1504            lifecycle.ticking_started.push(entity);
1505        }
1506        lifecycle
1507    }
1508
1509    fn apply_chunk_visibility_change(
1510        state: &mut ManagerState,
1511        chunk: ChunkPos,
1512        previous: EntityVisibility,
1513        new: EntityVisibility,
1514    ) -> EntityLifecycleChanges {
1515        let entity_ids = Self::entity_ids_in_chunk_order(state, chunk);
1516        let mut lifecycle = EntityLifecycleChanges::default();
1517
1518        for entity_id in entity_ids {
1519            let Some(entry) = state.live_by_id.get(&entity_id) else {
1520                continue;
1521            };
1522            if entry.ownership != EntityOwnership::ManagerOwned {
1523                continue;
1524            }
1525
1526            let old_visibility = Self::lifecycle_visibility_for(entry, previous);
1527            let new_visibility = Self::lifecycle_visibility_for(entry, new);
1528            if old_visibility == new_visibility {
1529                continue;
1530            }
1531
1532            let entity = entry.entity.clone();
1533            if old_visibility.is_ticking()
1534                && !new_visibility.is_ticking()
1535                && state.tick_list.remove(entity_id).is_some()
1536            {
1537                lifecycle.ticking_stopped.push(entity.clone());
1538            }
1539
1540            if old_visibility.is_accessible() && !new_visibility.is_accessible() {
1541                state.accessible_order.remove(entity_id);
1542                lifecycle.tracking_stopped.push(entity.clone());
1543            } else if !old_visibility.is_accessible() && new_visibility.is_accessible() {
1544                state.accessible_order.insert(entity_id);
1545                lifecycle.tracking_started.push(entity.clone());
1546            }
1547
1548            if !old_visibility.is_ticking()
1549                && new_visibility.is_ticking()
1550                && state.tick_list.add(&entity)
1551            {
1552                lifecycle.ticking_started.push(entity);
1553            }
1554        }
1555
1556        lifecycle
1557    }
1558
1559    fn is_entity_frozen_by_tick_rate(entity: &dyn Entity, runs_normally: bool) -> bool {
1560        !runs_normally
1561            && entity.entity_type() != &vanilla_entities::PLAYER
1562            && entity.count_player_passengers() == 0
1563    }
1564
1565    fn has_pending_world_change_in_vehicle_chain(entity: &SharedEntity) -> bool {
1566        if entity.is_world_change_pending() {
1567            return true;
1568        }
1569
1570        let mut visited = FxHashSet::default();
1571        visited.insert(entity.id());
1572        let mut vehicle = entity.vehicle();
1573        while let Some(current) = vehicle {
1574            assert!(
1575                visited.insert(current.id()),
1576                "cyclic passenger relationship involving entity {}",
1577                entity.id()
1578            );
1579            if current.is_world_change_pending() {
1580                return true;
1581            }
1582            vehicle = current.vehicle();
1583        }
1584        false
1585    }
1586
1587    fn is_accessible(state: &ManagerState, entry: &EntityEntry) -> bool {
1588        Self::is_accessible_at(state, entry.ownership, entry.chunk)
1589    }
1590
1591    fn is_accessible_at(state: &ManagerState, ownership: EntityOwnership, chunk: ChunkPos) -> bool {
1592        ownership == EntityOwnership::External
1593            || Self::chunk_visibility(state, chunk).is_accessible()
1594    }
1595
1596    fn is_valid_passenger_or_stop_riding(entity: &SharedEntity) -> bool {
1597        let Some(vehicle) = entity.vehicle() else {
1598            return false;
1599        };
1600
1601        if !vehicle.is_removed() && vehicle.has_passenger(entity.as_ref()) {
1602            Self::assert_acyclic_vehicle_chain(entity);
1603            return true;
1604        }
1605
1606        entity.stop_riding();
1607        false
1608    }
1609
1610    fn assert_acyclic_vehicle_chain(entity: &SharedEntity) {
1611        let mut visited = FxHashSet::default();
1612        visited.insert(entity.id());
1613
1614        let mut vehicle = entity.vehicle();
1615        while let Some(current) = vehicle {
1616            assert!(
1617                visited.insert(current.id()),
1618                "cyclic passenger relationship involving entity {}",
1619                entity.id()
1620            );
1621            vehicle = current.vehicle();
1622        }
1623    }
1624
1625    fn tick_non_passenger(
1626        &self,
1627        entity: &SharedEntity,
1628        ticked_entities: &mut FxHashSet<i32>,
1629        dirty_chunks: &mut FxHashSet<ChunkPos>,
1630    ) {
1631        snapshot_old_pos_and_rot_for_tick(entity.as_ref());
1632        entity.advance_tick_count();
1633        entity.tick();
1634        self.mark_dirty_after_tick(entity, dirty_chunks);
1635        self.tick_vehicle_passengers_with_ticked(entity.as_ref(), ticked_entities, dirty_chunks);
1636    }
1637
1638    fn tick_vehicle_passengers_with_ticked(
1639        &self,
1640        vehicle: &dyn Entity,
1641        ticked_entities: &mut FxHashSet<i32>,
1642        dirty_chunks: &mut FxHashSet<ChunkPos>,
1643    ) {
1644        let mut post_tick = |entity: &SharedEntity| {
1645            self.mark_dirty_after_tick(entity, dirty_chunks);
1646        };
1647        tick_vehicle_passengers_with_ticked_if(
1648            vehicle,
1649            ticked_entities,
1650            &mut post_tick,
1651            &mut |entity| self.can_tick_entity_now(entity.id()),
1652        );
1653    }
1654
1655    fn mark_dirty_after_tick(&self, entity: &SharedEntity, dirty_chunks: &mut FxHashSet<ChunkPos>) {
1656        if self.live_manager_owned_entity_chunk(entity.id()).is_some() {
1657            dirty_chunks.insert(ChunkPos::from_entity_pos(entity.position()));
1658        }
1659    }
1660
1661    fn can_tick_entity_now(&self, entity_id: i32) -> bool {
1662        let state = self.state.read();
1663        let Some(entry) = state.live_by_id.get(&entity_id) else {
1664            return false;
1665        };
1666        if Self::has_pending_world_change_in_vehicle_chain(&entry.entity) {
1667            return false;
1668        }
1669
1670        match entry.ownership {
1671            EntityOwnership::External => {
1672                entry.entity.entity_type() == &vanilla_entities::PLAYER
1673                    || state.tick_list.contains(entity_id)
1674            }
1675            EntityOwnership::ManagerOwned => state.tick_list.contains(entity_id),
1676        }
1677    }
1678
1679    fn insert_live_entry(state: &mut ManagerState, mut entry: EntityEntry) {
1680        let entity_id = entry.entity.id();
1681        entry.bounding_box = entry.entity.bounding_box();
1682        entry.spatial_cells = EntitySpatialCellBounds::from_aabb(&entry.bounding_box).cells();
1683        let is_accessible = Self::is_accessible_at(state, entry.ownership, entry.chunk);
1684        assert!(
1685            !state.live_by_id.contains_key(&entity_id),
1686            "entity id {entity_id} is already registered in the world entity manager"
1687        );
1688        assert!(
1689            state.live_by_uuid.insert(entry.uuid, entity_id).is_none(),
1690            "entity uuid {} is already registered in the world entity manager",
1691            entry.uuid
1692        );
1693        entry.section_order = Self::next_section_order(state);
1694        state
1695            .by_section
1696            .entry(PackedSectionPos::from(entry.section))
1697            .or_default()
1698            .insert(entity_id);
1699        Self::insert_into_spatial_cells(
1700            state,
1701            &entry.spatial_cells,
1702            entity_id,
1703            entry.query_order(),
1704        );
1705        state
1706            .by_chunk
1707            .entry(entry.chunk)
1708            .or_default()
1709            .insert(entity_id);
1710        state.live_by_id.insert(entity_id, entry);
1711        if is_accessible {
1712            state.accessible_order.insert(entity_id);
1713        }
1714    }
1715
1716    fn contains_uuid(state: &ManagerState, uuid: Uuid) -> bool {
1717        state.live_by_uuid.contains_key(&uuid)
1718            || state
1719                .unloading_by_chunk
1720                .values()
1721                .flatten()
1722                .any(|entry| entry.uuid == uuid)
1723            || state
1724                .save_pending_by_chunk
1725                .values()
1726                .flatten()
1727                .any(|entry| entry.uuid == uuid)
1728    }
1729
1730    fn contains_id(state: &ManagerState, entity_id: i32) -> bool {
1731        state.live_by_id.contains_key(&entity_id)
1732            || state
1733                .unloading_by_chunk
1734                .values()
1735                .flatten()
1736                .any(|entry| entry.entity.id() == entity_id)
1737            || state
1738                .save_pending_by_chunk
1739                .values()
1740                .flatten()
1741                .any(|entry| entry.entity.id() == entity_id)
1742    }
1743
1744    fn push_saveable_entity(
1745        result: &mut Vec<SharedEntity>,
1746        seen_ids: &mut FxHashSet<i32>,
1747        seen_uuids: &mut FxHashSet<Uuid>,
1748        entry: &EntityEntry,
1749    ) {
1750        if !entry.should_save() || !seen_ids.insert(entry.entity.id()) {
1751            return;
1752        }
1753        assert!(
1754            seen_uuids.insert(entry.uuid),
1755            "duplicate saveable entity uuid {} in world entity manager",
1756            entry.uuid
1757        );
1758        result.push(entry.entity.clone());
1759    }
1760
1761    fn push_unsaved_entity_report(
1762        saved_chunks: &FxHashSet<ChunkPos>,
1763        seen: &mut FxHashSet<i32>,
1764        reports: &mut Vec<UnsavedEntityReport>,
1765        entry: &EntityEntry,
1766    ) {
1767        if saved_chunks.contains(&entry.chunk)
1768            || !entry.should_save()
1769            || !seen.insert(entry.entity.id())
1770        {
1771            return;
1772        }
1773
1774        reports.push(UnsavedEntityReport {
1775            entity_id: entry.entity.id(),
1776            uuid: entry.uuid,
1777            chunk: entry.chunk,
1778        });
1779    }
1780
1781    fn remove_live_entry(state: &mut ManagerState, entity_id: i32) -> Option<EntityEntry> {
1782        let entry = state.live_by_id.remove(&entity_id)?;
1783        state.tick_list.remove(entity_id);
1784        state.live_by_uuid.remove(&entry.uuid);
1785        state.accessible_order.remove(entity_id);
1786        Self::remove_from_section(state, entry.section, entity_id);
1787        Self::remove_from_spatial_cells(state, &entry.spatial_cells, entity_id);
1788        Self::remove_from_chunk(state, entry.chunk, entity_id);
1789        Some(entry)
1790    }
1791
1792    fn next_section_order(state: &mut ManagerState) -> u64 {
1793        assert!(
1794            state.next_section_order < u64::MAX,
1795            "entity section insertion order exhausted"
1796        );
1797        let order = state.next_section_order;
1798        state.next_section_order += 1;
1799        order
1800    }
1801
1802    fn insert_into_spatial_cells(
1803        state: &mut ManagerState,
1804        cells: &[EntitySpatialCell],
1805        entity_id: i32,
1806        query_order: EntityQueryOrder,
1807    ) {
1808        for cell in cells {
1809            let insertion_index = state.by_spatial_cell.get(cell).map_or(0, |entity_ids| {
1810                let append_in_order = entity_ids.ids.last().is_none_or(|existing_id| {
1811                    state
1812                        .live_by_id
1813                        .get(existing_id)
1814                        .is_none_or(|entry| entry.query_order() <= query_order)
1815                });
1816                if append_in_order {
1817                    return entity_ids.ids.len();
1818                }
1819
1820                let earlier_index = entity_ids.iter().position(|existing_id| {
1821                    state
1822                        .live_by_id
1823                        .get(existing_id)
1824                        .is_some_and(|entry| entry.query_order() > query_order)
1825                });
1826                match earlier_index {
1827                    Some(index) => index,
1828                    None => entity_ids.ids.len(),
1829                }
1830            });
1831            state
1832                .by_spatial_cell
1833                .entry(*cell)
1834                .or_default()
1835                .insert_at(insertion_index, entity_id);
1836        }
1837    }
1838
1839    fn remove_from_spatial_cells(
1840        state: &mut ManagerState,
1841        cells: &[EntitySpatialCell],
1842        entity_id: i32,
1843    ) {
1844        for cell in cells {
1845            let remove_cell = if let Some(entity_ids) = state.by_spatial_cell.get_mut(cell) {
1846                entity_ids.remove(entity_id);
1847                entity_ids.is_empty()
1848            } else {
1849                false
1850            };
1851            if remove_cell {
1852                state.by_spatial_cell.remove(cell);
1853            }
1854        }
1855    }
1856
1857    fn remove_from_section(state: &mut ManagerState, section: SectionPos, entity_id: i32) {
1858        let packed = PackedSectionPos::from(section);
1859        let remove_section = if let Some(entity_ids) = state.by_section.get_mut(&packed) {
1860            entity_ids.remove(entity_id);
1861            entity_ids.is_empty()
1862        } else {
1863            false
1864        };
1865        if remove_section {
1866            state.by_section.remove(&packed);
1867        }
1868    }
1869
1870    fn remove_from_chunk(state: &mut ManagerState, chunk: ChunkPos, entity_id: i32) {
1871        let remove_chunk = if let Some(entity_ids) = state.by_chunk.get_mut(&chunk) {
1872            entity_ids.remove(&entity_id);
1873            entity_ids.is_empty()
1874        } else {
1875            false
1876        };
1877        if remove_chunk {
1878            state.by_chunk.remove(&chunk);
1879        }
1880    }
1881}
1882
1883impl Default for WorldEntityManager {
1884    fn default() -> Self {
1885        Self::new()
1886    }
1887}
1888
1889#[cfg(test)]
1890mod tests;