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, iter::FusedIterator, 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::{BlockPos, ChunkPos, PackedSectionPos, SectionPos, WorldAabb};
16use uuid::Uuid;
17
18use self::entries::{EntityEntries, EntitySlot};
19use self::spatial_query::EntityQueryEntries;
20use super::ticking::VehicleChain;
21use super::{
22    Entity, NullEntityCallback, RemovalReason, SharedEntity, snapshot_old_pos_and_rot_for_tick,
23    tick_vehicle_passengers_if,
24};
25
26// Vanilla treats four blocks as the largest ordinary entity search extent.
27const ENTITY_SPATIAL_CELL_SIZE: f64 = 4.0;
28
29// Center common integer positions inside cells instead of on cell boundaries.
30const ENTITY_SPATIAL_CELL_OFFSET: f64 = ENTITY_SPATIAL_CELL_SIZE / 2.0;
31
32// Large queries scan occupied cells instead of materializing an enormous cell range.
33const MAX_DIRECT_SPATIAL_CELL_PROBES: i128 = 4_096;
34const INLINE_QUERY_CAPACITY: usize = 32;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37struct EntitySpatialCell {
38    x: i32,
39    y: i32,
40    z: i32,
41}
42
43impl EntitySpatialCell {
44    fn containing(x: f64, y: f64, z: f64) -> Self {
45        Self {
46            x: ((x + ENTITY_SPATIAL_CELL_OFFSET) / ENTITY_SPATIAL_CELL_SIZE).floor() as i32,
47            y: ((y + ENTITY_SPATIAL_CELL_OFFSET) / ENTITY_SPATIAL_CELL_SIZE).floor() as i32,
48            z: ((z + ENTITY_SPATIAL_CELL_OFFSET) / ENTITY_SPATIAL_CELL_SIZE).floor() as i32,
49        }
50    }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
54struct EntityQueryOrder {
55    section: PackedSectionPos,
56    insertion: u64,
57}
58
59/// Candidate set used by vanilla movement's entity-shape collision query.
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub(crate) enum EntityCollisionCandidates {
62    /// Scan every spatially indexed entity because the source may broaden the default rule.
63    All,
64    /// Scan only entities that may be hard-collision targets.
65    HardCollisionRelevant,
66}
67
68impl EntityCollisionCandidates {
69    /// Selects the exact-safe broad phase for an optional movement source.
70    #[must_use]
71    pub(crate) fn for_source(source: Option<&dyn Entity>) -> Self {
72        if source.is_some_and(Entity::is_hard_collision_relevant) {
73            Self::All
74        } else {
75            Self::HardCollisionRelevant
76        }
77    }
78}
79
80/// Bounds an index update can leave in place when an entity's old and new cells overlap.
81#[derive(Debug, Clone, Copy)]
82struct RetainedSpatialCells {
83    /// Bounds of the old cells; new cells inside them are already indexed.
84    previous: Option<EntitySpatialCellBounds>,
85    /// Bounds of the new cells; old cells inside them stay indexed.
86    current: Option<EntitySpatialCellBounds>,
87}
88
89impl RetainedSpatialCells {
90    const NONE: Self = Self {
91        previous: None,
92        current: None,
93    };
94
95    const fn between(previous: &[EntitySpatialCell], current: &[EntitySpatialCell]) -> Self {
96        if !EntitySpatialCellBounds::cells_overlap(previous, current) {
97            return Self::NONE;
98        }
99
100        Self {
101            previous: EntitySpatialCellBounds::from_cells(previous),
102            current: EntitySpatialCellBounds::from_cells(current),
103        }
104    }
105}
106
107#[derive(Debug, Clone, Copy)]
108struct EntitySpatialCellBounds {
109    minimum: EntitySpatialCell,
110    maximum: EntitySpatialCell,
111}
112
113impl EntitySpatialCellBounds {
114    fn from_aabb(aabb: &WorldAabb) -> Self {
115        Self {
116            minimum: EntitySpatialCell::containing(aabb.min_x(), aabb.min_y(), aabb.min_z()),
117            maximum: EntitySpatialCell::containing(aabb.max_x(), aabb.max_y(), aabb.max_z()),
118        }
119    }
120
121    const fn from_cells(cells: &[EntitySpatialCell]) -> Option<Self> {
122        // `cells` is a complete rectangular range in x/y/z order, so its endpoints are the bounds.
123        let (Some(&minimum), Some(&maximum)) = (cells.first(), cells.last()) else {
124            return None;
125        };
126
127        Some(Self { minimum, maximum })
128    }
129
130    const fn cells_overlap(first: &[EntitySpatialCell], second: &[EntitySpatialCell]) -> bool {
131        let (Some(first), Some(second)) = (Self::from_cells(first), Self::from_cells(second))
132        else {
133            return false;
134        };
135
136        first.minimum.x <= second.maximum.x
137            && second.minimum.x <= first.maximum.x
138            && first.minimum.y <= second.maximum.y
139            && second.minimum.y <= first.maximum.y
140            && first.minimum.z <= second.maximum.z
141            && second.minimum.z <= first.maximum.z
142    }
143
144    const fn contains(self, cell: EntitySpatialCell) -> bool {
145        cell.x >= self.minimum.x
146            && cell.x <= self.maximum.x
147            && cell.y >= self.minimum.y
148            && cell.y <= self.maximum.y
149            && cell.z >= self.minimum.z
150            && cell.z <= self.maximum.z
151    }
152
153    fn direct_probe_count(self) -> i128 {
154        let width = i128::from(self.maximum.x) - i128::from(self.minimum.x) + 1;
155        let height = i128::from(self.maximum.y) - i128::from(self.minimum.y) + 1;
156        let depth = i128::from(self.maximum.z) - i128::from(self.minimum.z) + 1;
157        width * height * depth
158    }
159
160    fn cells(self) -> SmallVec<[EntitySpatialCell; 8]> {
161        let mut cells = SmallVec::new();
162        for x in self.minimum.x..=self.maximum.x {
163            for y in self.minimum.y..=self.maximum.y {
164                for z in self.minimum.z..=self.maximum.z {
165                    cells.push(EntitySpatialCell { x, y, z });
166                }
167            }
168        }
169        cells
170    }
171}
172
173/// Error returned when adding an entity to the runtime world fails.
174#[derive(Debug, Clone, PartialEq, Eq)]
175pub enum AddEntityError {
176    /// The entity is in a chunk that is not active in the world entity manager.
177    ChunkNotLoaded {
178        /// Entity network ID.
179        entity_id: i32,
180        /// Chunk containing the entity.
181        chunk: ChunkPos,
182    },
183    /// Another live entity with the same persistent UUID is already registered.
184    DuplicateUuid {
185        /// Entity network ID.
186        entity_id: i32,
187        /// Duplicate persistent UUID.
188        uuid: Uuid,
189    },
190    /// Another live entity with the same runtime network ID is already registered.
191    DuplicateId {
192        /// Duplicate entity network ID.
193        entity_id: i32,
194    },
195    /// The entity is already removed and cannot be added to the live world.
196    RemovedEntity {
197        /// Entity network ID.
198        entity_id: i32,
199    },
200}
201
202impl fmt::Display for AddEntityError {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        match self {
205            Self::ChunkNotLoaded { entity_id, chunk } => {
206                write!(f, "entity {entity_id} is in non-loaded chunk {chunk:?}")
207            }
208            Self::DuplicateUuid { entity_id, uuid } => {
209                write!(f, "entity {entity_id} has duplicate UUID {uuid}")
210            }
211            Self::DuplicateId { entity_id } => {
212                write!(f, "entity {entity_id} has a duplicate runtime ID")
213            }
214            Self::RemovedEntity { entity_id } => {
215                write!(f, "entity {entity_id} is already removed")
216            }
217        }
218    }
219}
220
221impl Error for AddEntityError {}
222
223/// Error returned when a live entity move cannot be committed.
224#[derive(Debug, Clone, PartialEq, Eq)]
225pub enum EntityMoveError {
226    /// The entity is no longer managed as live world state.
227    NotLive {
228        /// Entity network ID.
229        entity_id: i32,
230    },
231    /// The entity is deliberately frozen outside live world membership.
232    Inactive {
233        /// Entity network ID.
234        entity_id: i32,
235    },
236    /// The entity tried to move into a chunk outside active world ownership.
237    UnloadedDestination {
238        /// Entity network ID.
239        entity_id: i32,
240        /// Destination chunk.
241        chunk: ChunkPos,
242    },
243    /// invalid sleep block
244    InvalidSleepPosition {
245        /// entity id
246        entity_id: i32,
247        /// bed position
248        bed_position: BlockPos,
249    },
250}
251
252impl fmt::Display for EntityMoveError {
253    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254        match self {
255            Self::NotLive { entity_id } => {
256                write!(f, "entity {entity_id} is not live in the world")
257            }
258            Self::Inactive { entity_id } => {
259                write!(f, "entity {entity_id} is inactive outside live world state")
260            }
261            Self::UnloadedDestination { entity_id, chunk } => {
262                write!(
263                    f,
264                    "entity {entity_id} cannot move into non-loaded chunk {chunk:?}"
265                )
266            }
267            Self::InvalidSleepPosition {
268                entity_id,
269                bed_position,
270            } => {
271                write!(
272                    f,
273                    "entity {entity_id} cannot sleep at invalid bed position {bed_position:?}"
274                )
275            }
276        }
277    }
278}
279
280impl Error for EntityMoveError {}
281
282/// Whether the manager owns persistence for an entity.
283#[derive(Debug, Clone, Copy, PartialEq, Eq)]
284pub enum EntityOwnership {
285    /// Normal non-player entity owned by the world entity manager.
286    ManagerOwned,
287    /// Entity whose lifetime is owned elsewhere, such as a player.
288    External,
289}
290
291/// Entity visibility for a chunk column.
292///
293/// Mirrors vanilla `Visibility`: hidden chunks keep entity data inactive,
294/// tracked chunks expose entities to lookup/tracking, and ticking chunks also
295/// run manager-owned entity ticks.
296#[derive(Debug, Clone, Copy, PartialEq, Eq)]
297pub enum EntityVisibility {
298    /// Not accessible to entity lookup/tracking and not ticking.
299    Hidden,
300    /// Accessible to entity lookup/tracking but not ticking.
301    Tracked,
302    /// Accessible to entity lookup/tracking and ticking.
303    Ticking,
304}
305
306impl EntityVisibility {
307    /// Returns whether entities in this visibility are accessible to queries and tracking.
308    #[must_use]
309    pub const fn is_accessible(self) -> bool {
310        matches!(self, Self::Tracked | Self::Ticking)
311    }
312
313    /// Returns whether entities in this visibility are eligible for ticking.
314    #[must_use]
315    pub const fn is_ticking(self) -> bool {
316        matches!(self, Self::Ticking)
317    }
318}
319
320/// Entity lifecycle changes caused by manager membership or visibility updates.
321#[derive(Default)]
322pub struct EntityLifecycleChanges {
323    /// Entities that became tracked.
324    pub tracking_started: Vec<SharedEntity>,
325    /// Entities that stopped being tracked.
326    pub tracking_stopped: Vec<SharedEntity>,
327    /// Entities that entered the world entity tick list.
328    pub ticking_started: Vec<SharedEntity>,
329    /// Entities that left the world entity tick list.
330    pub ticking_stopped: Vec<SharedEntity>,
331}
332
333impl fmt::Debug for EntityLifecycleChanges {
334    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
335        f.debug_struct("EntityLifecycleChanges")
336            .field("tracking_started", &self.tracking_started.len())
337            .field("tracking_stopped", &self.tracking_stopped.len())
338            .field("ticking_started", &self.ticking_started.len())
339            .field("ticking_stopped", &self.ticking_stopped.len())
340            .finish()
341    }
342}
343
344impl EntityLifecycleChanges {
345    fn extend(&mut self, other: Self) {
346        self.tracking_started.extend(other.tracking_started);
347        self.tracking_stopped.extend(other.tracking_stopped);
348        self.ticking_started.extend(other.ticking_started);
349        self.ticking_stopped.extend(other.ticking_stopped);
350    }
351}
352
353/// Section/chunk membership update caused by a committed entity move.
354#[derive(Debug, Clone)]
355pub struct EntityMoveUpdate {
356    /// Entity network ID.
357    pub entity_id: i32,
358    /// Previous section membership.
359    pub old_section: SectionPos,
360    /// New section membership.
361    pub new_section: SectionPos,
362    /// Previous chunk membership.
363    pub old_chunk: ChunkPos,
364    /// New chunk membership.
365    pub new_chunk: ChunkPos,
366    /// Whether the entity was visible to normal world/tracker queries before the move.
367    pub old_accessible: bool,
368    /// Whether the entity is visible to normal world/tracker queries after the move.
369    pub new_accessible: bool,
370    /// Whether the manager-owned entity was in the tick list before the move.
371    pub old_ticking: bool,
372    /// Whether the manager-owned entity is in the tick list after the move.
373    pub new_ticking: bool,
374}
375
376impl EntityMoveUpdate {
377    /// Returns whether the entity changed sections.
378    #[must_use]
379    pub fn section_changed(&self) -> bool {
380        self.old_section != self.new_section
381    }
382
383    /// Returns whether the entity changed chunks.
384    #[must_use]
385    pub fn chunk_changed(&self) -> bool {
386        self.old_chunk != self.new_chunk
387    }
388
389    /// Returns whether the entity crossed an accessibility boundary.
390    #[must_use]
391    pub const fn accessibility_changed(&self) -> bool {
392        self.old_accessible != self.new_accessible
393    }
394
395    /// Returns whether this move made a previously hidden entity accessible.
396    #[must_use]
397    pub const fn became_accessible(&self) -> bool {
398        !self.old_accessible && self.new_accessible
399    }
400
401    /// Returns whether this move made a previously accessible entity hidden.
402    #[must_use]
403    pub const fn became_inaccessible(&self) -> bool {
404        self.old_accessible && !self.new_accessible
405    }
406
407    /// Returns whether this move made a previously non-ticking entity tick.
408    #[must_use]
409    pub const fn became_ticking(&self) -> bool {
410        !self.old_ticking && self.new_ticking
411    }
412
413    /// Returns whether this move made a previously ticking entity stop ticking.
414    #[must_use]
415    pub const fn became_non_ticking(&self) -> bool {
416        self.old_ticking && !self.new_ticking
417    }
418}
419
420/// Saveable entity that could not be persisted by a chunk save pass.
421#[derive(Debug, Clone, PartialEq, Eq)]
422pub struct UnsavedEntityReport {
423    /// Entity network ID.
424    pub entity_id: i32,
425    /// Entity persistent UUID.
426    pub uuid: Uuid,
427    /// Chunk containing the entity.
428    pub chunk: ChunkPos,
429}
430
431/// Entity changes produced when a chunk becomes loaded.
432#[derive(Default)]
433pub struct ChunkEntityLoadResult {
434    /// Retained entities restored to live world membership.
435    pub restored: Vec<SharedEntity>,
436    /// Live entities in this chunk whose tracking became visible again.
437    pub tracking_started: Vec<SharedEntity>,
438    /// Live entities in this chunk whose ticking became active again.
439    pub ticking_started: Vec<SharedEntity>,
440    /// Whether recovery created save-pending entity state for this chunk.
441    pub needs_save: bool,
442}
443
444/// Entity changes produced when a chunk starts unloading.
445#[derive(Default)]
446pub struct ChunkEntityUnloadStart {
447    /// Entities removed from live ownership and retained for chunk recovery.
448    pub retained: Vec<SharedEntity>,
449    /// Entities whose tracker visibility should stop for this chunk transition.
450    pub tracking_stopped: Vec<SharedEntity>,
451    /// Entities whose ticking should stop for this chunk transition.
452    pub ticking_stopped: Vec<SharedEntity>,
453}
454
455#[derive(Clone)]
456struct EntityEntry {
457    entity: SharedEntity,
458    _damage_history_owner: Arc<()>,
459    uuid: Uuid,
460    section: SectionPos,
461    chunk: ChunkPos,
462    bounding_box: WorldAabb,
463    spatial_cells: SmallVec<[EntitySpatialCell; 8]>,
464    hard_collision_relevant: bool,
465    query_order: EntityQueryOrder,
466    accessible: bool,
467    ownership: EntityOwnership,
468}
469
470/// Pre-tick manager state resolved under one state lock.
471struct TickContext {
472    manager_chunk: Option<ChunkPos>,
473}
474
475impl EntityEntry {
476    fn new(entity: SharedEntity, ownership: EntityOwnership) -> Self {
477        let section = SectionPos::from_entity_pos(entity.position());
478        let chunk = ChunkPos::new(section.x(), section.z());
479        let bounding_box = entity.bounding_box();
480        let hard_collision_relevant = entity.is_hard_collision_relevant();
481        Self {
482            _damage_history_owner: entity.base().damage_history().retain_owner(),
483            uuid: entity.uuid(),
484            entity,
485            section,
486            chunk,
487            bounding_box,
488            spatial_cells: EntitySpatialCellBounds::from_aabb(&bounding_box).cells(),
489            hard_collision_relevant,
490            query_order: EntityQueryOrder {
491                section: PackedSectionPos::from(section),
492                insertion: 0,
493            },
494            accessible: false,
495            ownership,
496        }
497    }
498
499    #[must_use]
500    fn should_save(&self) -> bool {
501        self.ownership == EntityOwnership::ManagerOwned
502            && (!self.entity.is_removed()
503                || self
504                    .entity
505                    .removal_reason()
506                    .is_some_and(RemovalReason::should_save))
507            && !self.entity.is_passenger()
508            && !self.entity.has_exactly_one_player_passenger()
509            && self.entity.entity_type().can_serialize
510    }
511}
512
513#[derive(Default)]
514struct ManagerState {
515    chunk_visibility: FxHashMap<ChunkPos, EntityVisibility>,
516    live_by_id: EntityEntries,
517    live_by_uuid: FxHashMap<Uuid, i32>,
518    accessible_order: IndexedOrderedEntityIds,
519    by_section: BTreeMap<PackedSectionPos, OrderedEntityIds>,
520    by_spatial_cell: FxHashMap<EntitySpatialCell, SpatialCellEntities>,
521    by_hard_collision_cell: FxHashMap<EntitySpatialCell, SpatialCellEntities>,
522    by_chunk: FxHashMap<ChunkPos, FxHashSet<i32>>,
523    unloading_by_chunk: FxHashMap<ChunkPos, Vec<EntityEntry>>,
524    save_pending_by_chunk: FxHashMap<ChunkPos, Vec<EntityEntry>>,
525    tick_list: EntityTickList,
526    next_section_order: u64,
527}
528
529#[derive(Default)]
530struct OrderedEntityIds {
531    ids: Vec<i32>,
532}
533
534#[derive(Default)]
535struct SpatialCellEntities {
536    entities: Vec<EntitySlot>,
537}
538
539impl OrderedEntityIds {
540    fn insert(&mut self, entity_id: i32) -> bool {
541        if self.ids.contains(&entity_id) {
542            return false;
543        }
544        self.ids.push(entity_id);
545        true
546    }
547
548    fn remove(&mut self, entity_id: i32) -> bool {
549        let Some(index) = self.ids.iter().position(|id| *id == entity_id) else {
550            return false;
551        };
552        self.ids.remove(index);
553
554        true
555    }
556
557    const fn is_empty(&self) -> bool {
558        self.ids.is_empty()
559    }
560
561    fn iter(&self) -> slice::Iter<'_, i32> {
562        self.ids.iter()
563    }
564}
565
566/// Append-ordered IDs with indexed unlinking for large lifecycle lists.
567///
568/// Free nodes are reused, and geometric compaction keeps removals amortized constant-time.
569#[derive(Default)]
570struct IndexedOrderedEntityIds {
571    nodes: Vec<OrderedEntityIdNode>,
572    node_by_id: FxHashMap<i32, usize>,
573    first: Option<usize>,
574    last: Option<usize>,
575    free: Option<usize>,
576}
577
578struct OrderedEntityIdNode {
579    entity_id: i32,
580    previous: Option<usize>,
581    next: Option<usize>,
582}
583
584impl IndexedOrderedEntityIds {
585    const MIN_COMPACTION_SIZE: usize = 64;
586
587    fn insert(&mut self, entity_id: i32) -> bool {
588        if self.node_by_id.contains_key(&entity_id) {
589            return false;
590        }
591        let previous = self.last;
592        let index = if let Some(free) = self.free {
593            self.free = self.nodes[free].next;
594            free
595        } else {
596            let index = self.nodes.len();
597            self.nodes.push(OrderedEntityIdNode {
598                entity_id,
599                previous: None,
600                next: None,
601            });
602            index
603        };
604
605        self.nodes[index] = OrderedEntityIdNode {
606            entity_id,
607            previous,
608            next: None,
609        };
610        let replaced = self.node_by_id.insert(entity_id, index);
611        debug_assert!(replaced.is_none());
612
613        if let Some(previous) = previous {
614            self.nodes[previous].next = Some(index);
615        } else {
616            self.first = Some(index);
617        }
618        self.last = Some(index);
619        true
620    }
621
622    fn remove(&mut self, entity_id: i32) -> bool {
623        let Some(index) = self.node_by_id.remove(&entity_id) else {
624            return false;
625        };
626
627        let previous = self.nodes[index].previous;
628        let next = self.nodes[index].next;
629        if let Some(previous) = previous {
630            self.nodes[previous].next = next;
631        } else {
632            self.first = next;
633        }
634        if let Some(next) = next {
635            self.nodes[next].previous = previous;
636        } else {
637            self.last = previous;
638        }
639
640        self.nodes[index] = OrderedEntityIdNode {
641            entity_id,
642            previous: None,
643            next: self.free,
644        };
645        self.free = Some(index);
646        self.compact_if_sparse();
647        true
648    }
649
650    fn len(&self) -> usize {
651        self.node_by_id.len()
652    }
653
654    fn iter(&self) -> IndexedOrderedEntityIdsIter<'_> {
655        IndexedOrderedEntityIdsIter {
656            nodes: &self.nodes,
657            next: self.first,
658            remaining: self.len(),
659        }
660    }
661
662    fn compact_if_sparse(&mut self) {
663        let live_len = self.len();
664        if self.nodes.len() < Self::MIN_COMPACTION_SIZE || live_len > self.nodes.len() / 2 {
665            return;
666        }
667
668        let mut compacted_nodes: Vec<OrderedEntityIdNode> = Vec::with_capacity(live_len);
669        let mut compacted_node_by_id = FxHashMap::default();
670        compacted_node_by_id.reserve(live_len);
671        let mut current = self.first;
672        while let Some(index) = current {
673            let node = &self.nodes[index];
674            let compacted_index = compacted_nodes.len();
675            let previous = compacted_index.checked_sub(1);
676            if let Some(previous) = previous {
677                compacted_nodes[previous].next = Some(compacted_index);
678            }
679            compacted_nodes.push(OrderedEntityIdNode {
680                entity_id: node.entity_id,
681                previous,
682                next: None,
683            });
684            compacted_node_by_id.insert(node.entity_id, compacted_index);
685            current = node.next;
686        }
687        debug_assert_eq!(compacted_nodes.len(), live_len);
688
689        self.nodes = compacted_nodes;
690        self.node_by_id = compacted_node_by_id;
691        self.first = (!self.nodes.is_empty()).then_some(0);
692        self.last = self.nodes.len().checked_sub(1);
693        self.free = None;
694    }
695}
696
697struct IndexedOrderedEntityIdsIter<'a> {
698    nodes: &'a [OrderedEntityIdNode],
699    next: Option<usize>,
700    remaining: usize,
701}
702
703impl<'a> Iterator for IndexedOrderedEntityIdsIter<'a> {
704    type Item = &'a i32;
705
706    fn next(&mut self) -> Option<Self::Item> {
707        let index = self.next?;
708        let node = &self.nodes[index];
709        self.next = node.next;
710        self.remaining -= 1;
711        Some(&node.entity_id)
712    }
713
714    fn size_hint(&self) -> (usize, Option<usize>) {
715        (self.remaining, Some(self.remaining))
716    }
717}
718
719impl ExactSizeIterator for IndexedOrderedEntityIdsIter<'_> {}
720impl FusedIterator for IndexedOrderedEntityIdsIter<'_> {}
721
722#[derive(Default)]
723struct EntityTickList {
724    active: FxHashMap<i32, SharedEntity>,
725    order: IndexedOrderedEntityIds,
726}
727
728impl EntityTickList {
729    fn add(&mut self, entity: &SharedEntity) -> bool {
730        let entity_id = entity.id();
731        if self.active.insert(entity_id, Arc::clone(entity)).is_some() {
732            return false;
733        }
734        self.order.insert(entity_id);
735        true
736    }
737
738    fn remove(&mut self, entity_id: i32) -> Option<SharedEntity> {
739        let removed = self.active.remove(&entity_id)?;
740        let removed_from_order = self.order.remove(entity_id);
741        debug_assert!(removed_from_order);
742        Some(removed)
743    }
744
745    fn contains(&self, entity_id: i32) -> bool {
746        self.active.contains_key(&entity_id)
747    }
748
749    fn snapshot(&self) -> Vec<SharedEntity> {
750        let mut snapshot = Vec::with_capacity(self.order.len());
751        for entity_id in self.order.iter() {
752            if let Some(entity) = self.active.get(entity_id) {
753                snapshot.push(Arc::clone(entity));
754            }
755        }
756        snapshot
757    }
758}
759
760/// Central world entity manager.
761pub struct WorldEntityManager {
762    state: SyncRwLock<ManagerState>,
763}
764
765impl fmt::Debug for WorldEntityManager {
766    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
767        let state = self.state.read();
768        f.debug_struct("WorldEntityManager")
769            .field("chunk_visibility", &state.chunk_visibility.len())
770            .field("live_entities", &state.live_by_id.len())
771            .field("unloading_chunks", &state.unloading_by_chunk.len())
772            .finish()
773    }
774}
775
776impl WorldEntityManager {
777    /// Creates an empty manager.
778    #[must_use]
779    pub fn new() -> Self {
780        Self {
781            state: SyncRwLock::new(ManagerState::default()),
782        }
783    }
784
785    /// Returns whether runtime entity ownership for this chunk is loaded.
786    ///
787    /// This is Vanilla's separate `areEntitiesLoaded` gate used by block-entity
788    /// ticking; it is intentionally not the stricter entity-ticking visibility.
789    #[must_use]
790    pub(crate) fn is_chunk_loaded(&self, pos: ChunkPos) -> bool {
791        self.state.read().chunk_visibility.contains_key(&pos)
792    }
793
794    /// Marks a chunk as loaded and reactivates retained unloading entities.
795    pub fn on_chunk_loaded(&self, pos: ChunkPos) -> ChunkEntityLoadResult {
796        let mut state = self.state.write();
797        state
798            .chunk_visibility
799            .entry(pos)
800            .or_insert(EntityVisibility::Hidden);
801
802        let mut result = ChunkEntityLoadResult::default();
803        if let Some(entries) = state.unloading_by_chunk.remove(&pos) {
804            result.restored.reserve(entries.len());
805            for entry in entries {
806                if entry.entity.is_removed() {
807                    if entry.should_save() {
808                        result.needs_save = true;
809                        state
810                            .save_pending_by_chunk
811                            .entry(pos)
812                            .or_default()
813                            .push(entry);
814                    }
815                    continue;
816                }
817
818                let entity = Arc::clone(&entry.entity);
819                Self::insert_live_entry(&mut state, entry);
820                let lifecycle = Self::apply_entity_lifecycle_after_insert(&mut state, entity.id());
821                result.tracking_started.extend(lifecycle.tracking_started);
822                result.ticking_started.extend(lifecycle.ticking_started);
823                result.restored.push(entity);
824            }
825        }
826
827        result
828    }
829
830    /// Updates the entity visibility for a chunk column.
831    pub fn update_chunk_visibility(
832        &self,
833        pos: ChunkPos,
834        visibility: EntityVisibility,
835    ) -> EntityLifecycleChanges {
836        let mut state = self.state.write();
837        let previous = state
838            .chunk_visibility
839            .insert(pos, visibility)
840            .unwrap_or(EntityVisibility::Hidden);
841
842        if previous == visibility {
843            return EntityLifecycleChanges::default();
844        }
845
846        Self::apply_chunk_visibility_change(&mut state, pos, previous, visibility)
847    }
848
849    fn push_unique_entity(
850        entity: &SharedEntity,
851        seen: &mut FxHashSet<i32>,
852        entities: &mut Vec<SharedEntity>,
853    ) {
854        if seen.insert(entity.id()) {
855            entities.push(Arc::clone(entity));
856        }
857    }
858
859    /// Moves manager-owned root entities in `pos` out of live world membership while
860    /// retaining them for possible chunk recovery.
861    pub fn begin_chunk_unload(&self, pos: ChunkPos) -> ChunkEntityUnloadStart {
862        let mut state = self.state.write();
863        let previous_visibility = state
864            .chunk_visibility
865            .remove(&pos)
866            .unwrap_or(EntityVisibility::Hidden);
867
868        let ids = Self::entity_ids_in_chunk_order(&state, pos);
869
870        let mut result = ChunkEntityUnloadStart::default();
871        let lifecycle = Self::apply_chunk_visibility_change(
872            &mut state,
873            pos,
874            previous_visibility,
875            EntityVisibility::Hidden,
876        );
877        let mut tracking_stopped_ids = lifecycle
878            .tracking_stopped
879            .iter()
880            .map(|entity| entity.id())
881            .collect::<FxHashSet<_>>();
882        result.tracking_stopped = lifecycle.tracking_stopped;
883        result.ticking_stopped = lifecycle.ticking_stopped;
884
885        let mut root_ids = Vec::new();
886
887        for entity_id in ids {
888            let Some(entry) = state.live_by_id.get(entity_id) else {
889                continue;
890            };
891            if entry.ownership != EntityOwnership::ManagerOwned {
892                continue;
893            }
894
895            Self::push_unique_entity(
896                &entry.entity,
897                &mut tracking_stopped_ids,
898                &mut result.tracking_stopped,
899            );
900            if !entry.entity.is_passenger() {
901                root_ids.push(entity_id);
902            }
903        }
904
905        let mut retained = Vec::new();
906        let mut visited = FxHashSet::default();
907        for entity_id in root_ids {
908            Self::retain_unloading_entity_tree(
909                &mut state,
910                entity_id,
911                &mut visited,
912                &mut retained,
913                &mut result.retained,
914                &mut tracking_stopped_ids,
915                &mut result.tracking_stopped,
916            );
917        }
918
919        if !retained.is_empty() {
920            state
921                .unloading_by_chunk
922                .entry(pos)
923                .or_default()
924                .extend(retained);
925        }
926
927        result
928    }
929
930    fn retain_unloading_entity_tree(
931        state: &mut ManagerState,
932        entity_id: i32,
933        visited: &mut FxHashSet<i32>,
934        retained: &mut Vec<EntityEntry>,
935        retained_entities: &mut Vec<SharedEntity>,
936        tracking_stopped_ids: &mut FxHashSet<i32>,
937        tracking_stopped: &mut Vec<SharedEntity>,
938    ) {
939        if !visited.insert(entity_id) {
940            return;
941        }
942
943        let Some(entry) = Self::remove_live_entry(state, entity_id) else {
944            return;
945        };
946
947        if entry.ownership != EntityOwnership::ManagerOwned {
948            let restored_id = entry.entity.id();
949            Self::insert_live_entry(state, entry);
950
951            let entity_to_tick = state.live_by_id.get(restored_id).and_then(|entry| {
952                let visibility = Self::lifecycle_visibility_for(
953                    entry,
954                    Self::chunk_visibility(state, entry.chunk),
955                );
956                visibility.is_ticking().then(|| Arc::clone(&entry.entity))
957            });
958
959            if let Some(entity) = entity_to_tick {
960                state.tick_list.add(&entity);
961            }
962
963            return;
964        }
965
966        let passengers = entry.entity.passengers();
967        Self::push_unique_entity(&entry.entity, tracking_stopped_ids, tracking_stopped);
968        retained_entities.push(Arc::clone(&entry.entity));
969        retained.push(entry);
970        for passenger in passengers {
971            Self::retain_unloading_entity_tree(
972                state,
973                passenger.id(),
974                visited,
975                retained,
976                retained_entities,
977                tracking_stopped_ids,
978                tracking_stopped,
979            );
980        }
981    }
982
983    /// Finalizes an unloading chunk. Retained entities are detached and dropped.
984    pub fn finalize_chunk_unload(&self, pos: ChunkPos) {
985        let entries = self
986            .state
987            .write()
988            .unloading_by_chunk
989            .remove(&pos)
990            .unwrap_or_default();
991
992        for entry in entries {
993            entry
994                .entity
995                .set_level_callback(Arc::new(NullEntityCallback));
996            entry.entity.set_removed(RemovalReason::UnloadedToChunk);
997        }
998    }
999
1000    /// Registers a live runtime entity.
1001    ///
1002    /// # Panics
1003    ///
1004    /// Panics if an entity with the same session network ID is already present. Duplicate runtime
1005    /// IDs indicate corrupted manager ownership and cannot be recovered without losing identity.
1006    pub fn add_live_entity(
1007        &self,
1008        entity: SharedEntity,
1009        ownership: EntityOwnership,
1010    ) -> Result<EntityLifecycleChanges, AddEntityError> {
1011        let entry = Self::checked_live_entry(entity, ownership)?;
1012        let entity_id = entry.entity.id();
1013        let mut state = self.state.write();
1014        assert!(
1015            !Self::contains_id(&state, entity_id),
1016            "entity id {entity_id} is already registered in the world entity manager"
1017        );
1018        Self::validate_live_entries(&state, slice::from_ref(&entry), ownership, true)?;
1019        Self::insert_live_entry(&mut state, entry);
1020        Ok(Self::apply_entity_lifecycle_after_insert(
1021            &mut state, entity_id,
1022        ))
1023    }
1024
1025    /// Adds a related group of live entities atomically.
1026    ///
1027    /// Use this for persisted vehicle/passenger trees so registration either
1028    /// publishes the whole tree or leaves world indexes unchanged.
1029    ///
1030    pub fn add_live_entity_tree(
1031        &self,
1032        entities: &[SharedEntity],
1033        ownership: EntityOwnership,
1034    ) -> Result<EntityLifecycleChanges, AddEntityError> {
1035        let mut entries = Vec::with_capacity(entities.len());
1036        for entity in entities {
1037            entries.push(Self::checked_live_entry(Arc::clone(entity), ownership)?);
1038        }
1039
1040        let mut seen_ids = FxHashSet::default();
1041        let mut seen_uuids = FxHashSet::default();
1042        for entry in &entries {
1043            let entity_id = entry.entity.id();
1044            if !seen_ids.insert(entity_id) {
1045                return Err(AddEntityError::DuplicateId { entity_id });
1046            }
1047            if !seen_uuids.insert(entry.uuid) {
1048                return Err(AddEntityError::DuplicateUuid {
1049                    entity_id,
1050                    uuid: entry.uuid,
1051                });
1052            }
1053        }
1054
1055        let mut state = self.state.write();
1056        Self::validate_live_entries(&state, &entries, ownership, false)?;
1057        let entity_ids = entries
1058            .iter()
1059            .map(|entry| entry.entity.id())
1060            .collect::<Vec<_>>();
1061        for entry in entries {
1062            Self::insert_live_entry(&mut state, entry);
1063        }
1064        let mut lifecycle = EntityLifecycleChanges::default();
1065        for entity_id in entity_ids {
1066            lifecycle.extend(Self::apply_entity_lifecycle_after_insert(
1067                &mut state, entity_id,
1068            ));
1069        }
1070        Ok(lifecycle)
1071    }
1072
1073    fn checked_live_entry(
1074        entity: SharedEntity,
1075        ownership: EntityOwnership,
1076    ) -> Result<EntityEntry, AddEntityError> {
1077        if entity.is_removed() {
1078            return Err(AddEntityError::RemovedEntity {
1079                entity_id: entity.id(),
1080            });
1081        }
1082
1083        Ok(EntityEntry::new(entity, ownership))
1084    }
1085
1086    fn validate_live_entries(
1087        state: &ManagerState,
1088        entries: &[EntityEntry],
1089        ownership: EntityOwnership,
1090        require_loaded_chunks: bool,
1091    ) -> Result<(), AddEntityError> {
1092        for entry in entries {
1093            let entity_id = entry.entity.id();
1094            if Self::contains_id(state, entity_id) {
1095                return Err(AddEntityError::DuplicateId { entity_id });
1096            }
1097            if Self::contains_uuid(state, entry.uuid) {
1098                return Err(AddEntityError::DuplicateUuid {
1099                    entity_id,
1100                    uuid: entry.uuid,
1101                });
1102            }
1103            if require_loaded_chunks
1104                && ownership == EntityOwnership::ManagerOwned
1105                && !state.chunk_visibility.contains_key(&entry.chunk)
1106            {
1107                return Err(AddEntityError::ChunkNotLoaded {
1108                    entity_id,
1109                    chunk: entry.chunk,
1110                });
1111            }
1112        }
1113        Ok(())
1114    }
1115
1116    /// Removes a live entity for an explicit entity removal reason.
1117    pub fn remove_live_entity(
1118        &self,
1119        entity_id: i32,
1120        reason: RemovalReason,
1121    ) -> Option<SharedEntity> {
1122        let mut state = self.state.write();
1123        let entry = Self::remove_live_entry(&mut state, entity_id)?;
1124        let entity = Arc::clone(&entry.entity);
1125
1126        if reason.should_save() && entry.should_save() {
1127            state
1128                .save_pending_by_chunk
1129                .entry(entry.chunk)
1130                .or_default()
1131                .push(entry);
1132        }
1133
1134        Some(entity)
1135    }
1136
1137    /// Acknowledges that selected save-pending entities for `chunk` were persisted.
1138    pub fn on_chunk_saved(&self, chunk: ChunkPos, saved_entity_ids: &[i32]) {
1139        if saved_entity_ids.is_empty() {
1140            return;
1141        }
1142
1143        let saved_entity_ids = saved_entity_ids.iter().copied().collect::<FxHashSet<_>>();
1144        let mut state = self.state.write();
1145        let Some(entries) = state.save_pending_by_chunk.get_mut(&chunk) else {
1146            return;
1147        };
1148
1149        entries.retain(|entry| !saved_entity_ids.contains(&entry.entity.id()));
1150        if entries.is_empty() {
1151            state.save_pending_by_chunk.remove(&chunk);
1152        }
1153    }
1154
1155    /// Returns whether `chunk` has removed runtime entities waiting for a save acknowledgement.
1156    #[must_use]
1157    pub fn has_save_pending_for_chunk(&self, chunk: ChunkPos) -> bool {
1158        self.state
1159            .read()
1160            .save_pending_by_chunk
1161            .get(&chunk)
1162            .is_some_and(|entries| !entries.is_empty())
1163    }
1164
1165    /// Validates that a live entity can move to `new_pos`.
1166    pub fn validate_move(&self, entity_id: i32, new_pos: DVec3) -> Result<(), EntityMoveError> {
1167        let state = self.state.read();
1168
1169        let Some(entry) = state.live_by_id.get(entity_id) else {
1170            return Err(EntityMoveError::NotLive { entity_id });
1171        };
1172
1173        if entry.ownership == EntityOwnership::ManagerOwned {
1174            let new_section = SectionPos::from_entity_pos(new_pos);
1175            let new_chunk = ChunkPos::new(new_section.x(), new_section.z());
1176            if !Self::can_move_manager_owned_to_chunk(&state, entry, new_chunk) {
1177                return Err(EntityMoveError::UnloadedDestination {
1178                    entity_id,
1179                    chunk: new_chunk,
1180                });
1181            }
1182        }
1183
1184        Ok(())
1185    }
1186
1187    /// Commits manager indexes after a live entity position change.
1188    #[expect(
1189        clippy::too_many_lines,
1190        reason = "keeps movement index updates atomic under one manager write lock"
1191    )]
1192    pub fn commit_move(
1193        &self,
1194        entity_id: i32,
1195        new_pos: DVec3,
1196    ) -> Result<EntityMoveUpdate, EntityMoveError> {
1197        let mut state = self.state.write();
1198
1199        let Some(current) = state.live_by_id.get(entity_id) else {
1200            return Err(EntityMoveError::NotLive { entity_id });
1201        };
1202
1203        let new_section = SectionPos::from_entity_pos(new_pos);
1204        let new_chunk = ChunkPos::new(new_section.x(), new_section.z());
1205
1206        if current.ownership == EntityOwnership::ManagerOwned
1207            && !Self::can_move_manager_owned_to_chunk(&state, current, new_chunk)
1208        {
1209            return Err(EntityMoveError::UnloadedDestination {
1210                entity_id,
1211                chunk: new_chunk,
1212            });
1213        }
1214
1215        let old_section = current.section;
1216        let old_chunk = current.chunk;
1217        let old_accessible = Self::is_accessible(&state, current);
1218        let new_accessible = Self::is_accessible_at(&state, current.ownership, new_chunk);
1219        let old_visibility =
1220            Self::lifecycle_visibility_for(current, Self::chunk_visibility(&state, old_chunk));
1221        let new_visibility =
1222            Self::lifecycle_visibility_for(current, Self::chunk_visibility(&state, new_chunk));
1223        let old_ticking = old_visibility.is_ticking();
1224        let new_ticking = new_visibility.is_ticking();
1225
1226        let new_bounding_box = current.entity.bounding_box();
1227        let new_spatial_cells = EntitySpatialCellBounds::from_aabb(&new_bounding_box).cells();
1228        let spatial_cells_changed = current.spatial_cells != new_spatial_cells;
1229        let section_changed = old_section != new_section;
1230        let spatial_index_changed = spatial_cells_changed || section_changed;
1231        let current_section_order = current.query_order.insertion;
1232        let hard_collision_relevant = current.hard_collision_relevant;
1233
1234        let Some(entity_slot) = state.live_by_id.slot(entity_id) else {
1235            return Err(EntityMoveError::NotLive { entity_id });
1236        };
1237
1238        if !section_changed && !spatial_cells_changed {
1239            if let Some(entry) = state.live_by_id.get_mut(entity_id) {
1240                entry.bounding_box = new_bounding_box;
1241            }
1242
1243            return Ok(EntityMoveUpdate {
1244                entity_id,
1245                old_section,
1246                new_section,
1247                old_chunk,
1248                new_chunk,
1249                old_accessible,
1250                new_accessible,
1251                old_ticking,
1252                new_ticking,
1253            });
1254        }
1255
1256        // A new section changes the query order, so every cell has to be re-sorted.
1257        let retained = if section_changed {
1258            RetainedSpatialCells::NONE
1259        } else {
1260            RetainedSpatialCells::between(&current.spatial_cells, &new_spatial_cells)
1261        };
1262
1263        if section_changed {
1264            Self::remove_from_section(&mut state, old_section, entity_id);
1265            Self::remove_from_chunk(&mut state, old_chunk, entity_id);
1266        }
1267
1268        let new_section_order = if section_changed {
1269            Self::next_section_order(&mut state)
1270        } else {
1271            current_section_order
1272        };
1273
1274        if spatial_index_changed {
1275            let Some(entry) = state.live_by_id.get_mut(entity_id) else {
1276                return Err(EntityMoveError::NotLive { entity_id });
1277            };
1278
1279            let previous_cells = mem::take(&mut entry.spatial_cells);
1280            Self::remove_from_spatial_indexes(
1281                &mut state,
1282                &previous_cells,
1283                entity_slot,
1284                hard_collision_relevant,
1285                retained.current,
1286            );
1287        }
1288
1289        if let Some(entry) = state.live_by_id.get_mut(entity_id) {
1290            entry.section = new_section;
1291            entry.chunk = new_chunk;
1292            entry.bounding_box = new_bounding_box;
1293            entry.accessible = new_accessible;
1294
1295            if spatial_index_changed {
1296                entry.spatial_cells = new_spatial_cells;
1297            }
1298
1299            if section_changed {
1300                entry.query_order = EntityQueryOrder {
1301                    section: PackedSectionPos::from(new_section),
1302                    insertion: new_section_order,
1303                };
1304            }
1305        }
1306
1307        if spatial_index_changed {
1308            Self::insert_into_spatial_indexes(
1309                &mut state,
1310                entity_slot,
1311                hard_collision_relevant,
1312                retained.previous,
1313            );
1314        }
1315
1316        if section_changed {
1317            state
1318                .by_section
1319                .entry(PackedSectionPos::from(new_section))
1320                .or_default()
1321                .insert(entity_id);
1322            state
1323                .by_chunk
1324                .entry(new_chunk)
1325                .or_default()
1326                .insert(entity_id);
1327        }
1328
1329        if old_accessible && !new_accessible {
1330            state.accessible_order.remove(entity_id);
1331        } else if !old_accessible && new_accessible {
1332            state.accessible_order.insert(entity_id);
1333        }
1334
1335        if old_ticking && !new_ticking {
1336            state.tick_list.remove(entity_id);
1337        } else if !old_ticking && new_ticking {
1338            let ManagerState {
1339                live_by_id,
1340                tick_list,
1341                ..
1342            } = &mut *state;
1343
1344            if let Some(entry) = live_by_id.get(entity_id) {
1345                tick_list.add(&entry.entity);
1346            }
1347        }
1348
1349        Ok(EntityMoveUpdate {
1350            entity_id,
1351            old_section,
1352            new_section,
1353            old_chunk,
1354            new_chunk,
1355            old_accessible,
1356            new_accessible,
1357            old_ticking,
1358            new_ticking,
1359        })
1360    }
1361
1362    /// Commits a live entity bounding-box change to the spatial query index.
1363    ///
1364    /// Reads the current bounds after acquiring the manager lock so concurrent
1365    /// callbacks may complete in either order without restoring stale bounds.
1366    pub fn commit_bounding_box_change(&self, entity_id: i32) {
1367        let mut state = self.state.write();
1368
1369        let Some(current) = state.live_by_id.get(entity_id) else {
1370            return;
1371        };
1372
1373        let bounding_box = current.entity.bounding_box();
1374        let new_spatial_cells = EntitySpatialCellBounds::from_aabb(&bounding_box).cells();
1375        let hard_collision_relevant = current.hard_collision_relevant;
1376
1377        if current.spatial_cells == new_spatial_cells {
1378            if let Some(entry) = state.live_by_id.get_mut(entity_id) {
1379                entry.bounding_box = bounding_box;
1380            }
1381
1382            return;
1383        }
1384
1385        let retained = RetainedSpatialCells::between(&current.spatial_cells, &new_spatial_cells);
1386
1387        let Some(entry) = state.live_by_id.get_mut(entity_id) else {
1388            return;
1389        };
1390
1391        let previous_cells = mem::take(&mut entry.spatial_cells);
1392
1393        let Some(entity_slot) = state.live_by_id.slot(entity_id) else {
1394            return;
1395        };
1396
1397        Self::remove_from_spatial_indexes(
1398            &mut state,
1399            &previous_cells,
1400            entity_slot,
1401            hard_collision_relevant,
1402            retained.current,
1403        );
1404
1405        if let Some(entry) = state.live_by_id.get_mut(entity_id) {
1406            entry.bounding_box = bounding_box;
1407            entry.spatial_cells = new_spatial_cells;
1408        }
1409
1410        Self::insert_into_spatial_indexes(
1411            &mut state,
1412            entity_slot,
1413            hard_collision_relevant,
1414            retained.previous,
1415        );
1416    }
1417
1418    fn can_move_manager_owned_to_chunk(
1419        state: &ManagerState,
1420        entry: &EntityEntry,
1421        new_chunk: ChunkPos,
1422    ) -> bool {
1423        state.chunk_visibility.contains_key(&new_chunk)
1424            || (entry.entity.is_passenger()
1425                && Self::has_live_loaded_root_vehicle(state, &entry.entity))
1426    }
1427
1428    fn has_live_loaded_root_vehicle(state: &ManagerState, entity: &SharedEntity) -> bool {
1429        let mut visited = FxHashSet::default();
1430        visited.insert(entity.id());
1431
1432        let mut passenger = Arc::clone(entity);
1433        let Some(mut vehicle) = passenger.vehicle() else {
1434            return false;
1435        };
1436
1437        loop {
1438            assert!(
1439                visited.insert(vehicle.id()),
1440                "cyclic passenger relationship involving entity {}",
1441                entity.id()
1442            );
1443            if vehicle.is_removed() || !vehicle.has_passenger(passenger.as_ref()) {
1444                return false;
1445            }
1446
1447            let Some(vehicle_entry) = state.live_by_id.get(vehicle.id()) else {
1448                return false;
1449            };
1450
1451            let Some(next_vehicle) = vehicle.vehicle() else {
1452                return match vehicle_entry.ownership {
1453                    EntityOwnership::External => true,
1454                    EntityOwnership::ManagerOwned => {
1455                        state.chunk_visibility.contains_key(&vehicle_entry.chunk)
1456                    }
1457                };
1458            };
1459
1460            passenger = vehicle;
1461            vehicle = next_vehicle;
1462        }
1463    }
1464
1465    #[must_use]
1466    /// Gets a live entity by session network ID.
1467    pub fn get_by_id(&self, entity_id: i32) -> Option<SharedEntity> {
1468        self.state
1469            .read()
1470            .live_by_id
1471            .get(entity_id)
1472            .map(|entry| Arc::clone(&entry.entity))
1473    }
1474
1475    /// Returns true if this exact entity is live or retained for chunk-unload recovery.
1476    pub fn contains_live_or_unloading_entity(&self, entity: &SharedEntity) -> bool {
1477        let state = self.state.read();
1478        state
1479            .live_by_id
1480            .get(entity.id())
1481            .is_some_and(|entry| Arc::ptr_eq(&entry.entity, entity))
1482            || state
1483                .unloading_by_chunk
1484                .values()
1485                .flatten()
1486                .any(|entry| Arc::ptr_eq(&entry.entity, entity))
1487    }
1488
1489    #[must_use]
1490    /// Gets a live entity by session network ID if it is visible to vanilla gameplay lookups.
1491    pub fn get_accessible_by_id(&self, entity_id: i32) -> Option<SharedEntity> {
1492        let state = self.state.read();
1493        let entry = state.live_by_id.get(entity_id)?;
1494        entry.accessible.then(|| Arc::clone(&entry.entity))
1495    }
1496
1497    #[must_use]
1498    /// Gets a live entity by persistent UUID.
1499    pub fn get_by_uuid(&self, uuid: &Uuid) -> Option<SharedEntity> {
1500        let state = self.state.read();
1501        let entity_id = state.live_by_uuid.get(uuid)?;
1502        state
1503            .live_by_id
1504            .get(*entity_id)
1505            .map(|entry| Arc::clone(&entry.entity))
1506    }
1507
1508    #[must_use]
1509    /// Gets live entities whose bounding boxes intersect `aabb` and match `predicate`.
1510    pub fn get_entities_in_aabb_matching(
1511        &self,
1512        aabb: &WorldAabb,
1513        mut predicate: impl FnMut(&dyn Entity) -> bool,
1514    ) -> Vec<SharedEntity> {
1515        self.get_entities_in_aabb(aabb)
1516            .into_iter()
1517            .filter(|entity| predicate(entity.as_ref()))
1518            .collect()
1519    }
1520
1521    /// Returns whether any live entity intersects `aabb` and matches `predicate`.
1522    #[must_use]
1523    pub fn has_entity_in_aabb_matching(
1524        &self,
1525        aabb: &WorldAabb,
1526        mut predicate: impl FnMut(&dyn Entity) -> bool,
1527    ) -> bool {
1528        let state = self.state.read();
1529
1530        for entry in Self::entity_query_entries(&state, aabb) {
1531            if entry.accessible && predicate(entry.entity.as_ref()) {
1532                return true;
1533            }
1534        }
1535
1536        false
1537    }
1538
1539    /// Gets matching live entity bounding boxes that intersect `aabb`.
1540    #[must_use]
1541    pub fn get_entity_bounding_boxes_in_aabb_matching(
1542        &self,
1543        aabb: &WorldAabb,
1544        mut predicate: impl FnMut(&dyn Entity) -> bool,
1545    ) -> Vec<WorldAabb> {
1546        let state = self.state.read();
1547        let mut result = Vec::new();
1548
1549        for entry in Self::entity_query_entries(&state, aabb) {
1550            if entry.accessible && predicate(entry.entity.as_ref()) {
1551                result.push(entry.bounding_box);
1552            }
1553        }
1554
1555        result
1556    }
1557
1558    /// Gets movement-collision bounding boxes from the selected conservative broad phase.
1559    ///
1560    /// Entity callbacks run after the manager read lock is released. This permits plugin-defined
1561    /// collision rules to perform ordinary entity-manager lookups without lock reentrancy.
1562    #[must_use]
1563    pub(crate) fn get_movement_collision_boxes_in_aabb_matching(
1564        &self,
1565        aabb: &WorldAabb,
1566        candidates: EntityCollisionCandidates,
1567        mut predicate: impl FnMut(&dyn Entity) -> bool,
1568    ) -> Vec<WorldAabb> {
1569        let colliding_entities = self
1570            .movement_collision_candidate_snapshot(aabb, candidates)
1571            .into_iter()
1572            .filter(|entity| entity.bounding_box().intersects(*aabb) && predicate(entity.as_ref()))
1573            .collect::<SmallVec<[SharedEntity; 8]>>();
1574
1575        // Vanilla first completes `getEntities`, including every selector callback, and only then
1576        // reads the accepted entities' boxes. Keep those phases separate so a plugin callback that
1577        // moves an entity cannot leave a stale collision shape in the result.
1578        colliding_entities
1579            .into_iter()
1580            .map(|entity| entity.bounding_box())
1581            .collect()
1582    }
1583
1584    /// Returns whether a movement-collision candidate matches after the broad phase.
1585    #[must_use]
1586    pub(crate) fn has_movement_collision_in_aabb_matching(
1587        &self,
1588        aabb: &WorldAabb,
1589        candidates: EntityCollisionCandidates,
1590        mut predicate: impl FnMut(&dyn Entity) -> bool,
1591    ) -> bool {
1592        self.movement_collision_candidate_snapshot(aabb, candidates)
1593            .into_iter()
1594            .any(|entity| entity.bounding_box().intersects(*aabb) && predicate(entity.as_ref()))
1595    }
1596
1597    fn movement_collision_candidate_snapshot(
1598        &self,
1599        aabb: &WorldAabb,
1600        candidates: EntityCollisionCandidates,
1601    ) -> SmallVec<[SharedEntity; 8]> {
1602        let state = self.state.read();
1603        let mut snapshot = SmallVec::new();
1604
1605        Self::query_entries(&state, aabb, candidates, None).for_each(|entry| {
1606            if entry.accessible {
1607                snapshot.push(Arc::clone(&entry.entity));
1608            }
1609        });
1610
1611        snapshot
1612    }
1613
1614    #[must_use]
1615    /// Gets the nearest live entity whose bounding box intersects `aabb` and matches `predicate`.
1616    pub fn nearest_entity_in_aabb_matching(
1617        &self,
1618        aabb: &WorldAabb,
1619        origin: DVec3,
1620        mut predicate: impl FnMut(&dyn Entity) -> bool,
1621    ) -> Option<SharedEntity> {
1622        self.get_entities_in_aabb(aabb)
1623            .into_iter()
1624            .filter(|entity| predicate(entity.as_ref()))
1625            .min_by(|first, second| {
1626                first
1627                    .position()
1628                    .distance_squared(origin)
1629                    .total_cmp(&second.position().distance_squared(origin))
1630            })
1631    }
1632
1633    #[must_use]
1634    /// Gets live entities whose bounding boxes intersect `aabb`.
1635    pub fn get_entities_in_aabb(&self, aabb: &WorldAabb) -> Vec<SharedEntity> {
1636        let state = self.state.read();
1637        let mut result = Vec::new();
1638
1639        Self::entity_query_entries(&state, aabb).for_each(|entry| {
1640            if entry.accessible {
1641                result.push(Arc::clone(&entry.entity));
1642            }
1643        });
1644
1645        result
1646    }
1647
1648    /// Runs `action` on the ordered hits borrowed under the read lock, returning `None` when
1649    /// it handled them. Returns the hits as owned entities when `action` declines or more
1650    /// than `borrow_limit` entities match.
1651    /// `action` must not re-enter the manager or write spatial or lifecycle state.
1652    pub(crate) fn try_with_entities_in_aabb(
1653        &self,
1654        aabb: &WorldAabb,
1655        borrow_limit: Option<usize>,
1656        action: impl FnOnce(&[&SharedEntity]) -> bool,
1657    ) -> Option<Vec<SharedEntity>> {
1658        let state = self.state.read();
1659        let mut entries = Self::entity_query_entries(&state, aabb).filter(|entry| entry.accessible);
1660        let mut entities = SmallVec::<[&SharedEntity; INLINE_QUERY_CAPACITY]>::new();
1661
1662        if let Some(limit) = borrow_limit {
1663            entries
1664                .by_ref()
1665                .take(limit)
1666                .for_each(|entry| entities.push(&entry.entity));
1667
1668            if let Some(next) = entries.next() {
1669                let mut snapshot = Vec::with_capacity(entities.capacity());
1670                snapshot.extend(entities.iter().map(|entity| Arc::clone(entity)));
1671                snapshot.push(Arc::clone(&next.entity));
1672                entries.for_each(|entry| snapshot.push(Arc::clone(&entry.entity)));
1673
1674                return Some(snapshot);
1675            }
1676        } else {
1677            entries.for_each(|entry| entities.push(&entry.entity));
1678        }
1679
1680        if action(&entities) {
1681            None
1682        } else {
1683            Some(entities.into_iter().map(Arc::clone).collect())
1684        }
1685    }
1686
1687    /// Gets all live entities visible to vanilla gameplay lookups.
1688    #[must_use]
1689    pub fn get_accessible_entities(&self) -> Vec<SharedEntity> {
1690        let state = self.state.read();
1691        state
1692            .accessible_order
1693            .iter()
1694            .filter_map(|entity_id| state.live_by_id.get(*entity_id))
1695            .filter(|entry| entry.accessible)
1696            .map(|entry| Arc::clone(&entry.entity))
1697            .collect()
1698    }
1699
1700    fn entity_query_entries<'a>(
1701        state: &'a ManagerState,
1702        aabb: &WorldAabb,
1703    ) -> EntityQueryEntries<'a> {
1704        Self::query_entries(state, aabb, EntityCollisionCandidates::All, Some(aabb))
1705    }
1706
1707    fn query_entries<'a>(
1708        state: &'a ManagerState,
1709        aabb: &WorldAabb,
1710        candidates: EntityCollisionCandidates,
1711        entry_bounds: Option<&WorldAabb>,
1712    ) -> EntityQueryEntries<'a> {
1713        let spatial_index = match candidates {
1714            EntityCollisionCandidates::All => &state.by_spatial_cell,
1715            EntityCollisionCandidates::HardCollisionRelevant => &state.by_hard_collision_cell,
1716        };
1717
1718        let bounds = EntitySpatialCellBounds::from_aabb(aabb);
1719        let mut populated_cells = SmallVec::new();
1720        let direct_probe_limit =
1721            MAX_DIRECT_SPATIAL_CELL_PROBES.min(spatial_index.capacity() as i128);
1722
1723        if bounds.direct_probe_count() <= direct_probe_limit {
1724            for x in bounds.minimum.x..=bounds.maximum.x {
1725                for y in bounds.minimum.y..=bounds.maximum.y {
1726                    for z in bounds.minimum.z..=bounds.maximum.z {
1727                        if let Some(cell) = spatial_index.get(&EntitySpatialCell { x, y, z }) {
1728                            populated_cells.push(cell.entities.as_slice());
1729                        }
1730                    }
1731                }
1732            }
1733        } else {
1734            for (cell, entries) in spatial_index {
1735                if bounds.contains(*cell) {
1736                    populated_cells.push(entries.entities.as_slice());
1737                }
1738            }
1739        }
1740
1741        EntityQueryEntries::new(state, entry_bounds, populated_cells)
1742    }
1743
1744    fn entity_ids_in_chunk_order(state: &ManagerState, chunk: ChunkPos) -> Vec<i32> {
1745        let first = PackedSectionPos::from(SectionPos::new(chunk.0.x, 0, chunk.0.y));
1746        let last = PackedSectionPos::from(SectionPos::new(chunk.0.x, -1, chunk.0.y));
1747        state
1748            .by_section
1749            .range(first..=last)
1750            .flat_map(|(_, entity_ids)| entity_ids.iter().copied())
1751            .collect()
1752    }
1753
1754    /// Reports saveable entities whose chunks were not part of a chunk save pass.
1755    #[must_use]
1756    pub fn saveable_entities_outside_chunks(
1757        &self,
1758        saved_chunks: &[ChunkPos],
1759    ) -> Vec<UnsavedEntityReport> {
1760        let saved_chunks = saved_chunks.iter().copied().collect::<FxHashSet<_>>();
1761        let state = self.state.read();
1762        let mut seen = FxHashSet::default();
1763        let mut reports = Vec::new();
1764
1765        for entry in state.live_by_id.values() {
1766            Self::push_unsaved_entity_report(&saved_chunks, &mut seen, &mut reports, entry);
1767        }
1768
1769        for entries in state.unloading_by_chunk.values() {
1770            for entry in entries {
1771                Self::push_unsaved_entity_report(&saved_chunks, &mut seen, &mut reports, entry);
1772            }
1773        }
1774
1775        for entries in state.save_pending_by_chunk.values() {
1776            for entry in entries {
1777                Self::push_unsaved_entity_report(&saved_chunks, &mut seen, &mut reports, entry);
1778            }
1779        }
1780
1781        reports.sort_by_key(|report| (report.chunk.0.x, report.chunk.0.y, report.entity_id));
1782        reports
1783    }
1784
1785    #[must_use]
1786    /// Gets entities that should be serialized for `chunk`.
1787    pub fn get_saveable_entities_for_chunk(&self, chunk: ChunkPos) -> Vec<SharedEntity> {
1788        let state = self.state.read();
1789        let mut result = Vec::new();
1790        let mut seen_ids = FxHashSet::default();
1791        let mut seen_uuids = FxHashSet::default();
1792
1793        for entity_id in Self::entity_ids_in_chunk_order(&state, chunk) {
1794            let Some(entry) = state.live_by_id.get(entity_id) else {
1795                continue;
1796            };
1797            Self::push_saveable_entity(&mut result, &mut seen_ids, &mut seen_uuids, entry);
1798        }
1799
1800        if let Some(entries) = state.unloading_by_chunk.get(&chunk) {
1801            for entry in entries {
1802                Self::push_saveable_entity(&mut result, &mut seen_ids, &mut seen_uuids, entry);
1803            }
1804        }
1805
1806        if let Some(entries) = state.save_pending_by_chunk.get(&chunk) {
1807            for entry in entries {
1808                Self::push_saveable_entity(&mut result, &mut seen_ids, &mut seen_uuids, entry);
1809            }
1810        }
1811
1812        result
1813    }
1814
1815    #[must_use]
1816    /// Gets live entities currently indexed in `chunk`.
1817    pub fn live_entities_in_chunk(&self, chunk: ChunkPos) -> Vec<SharedEntity> {
1818        let state = self.state.read();
1819        Self::entity_ids_in_chunk_order(&state, chunk)
1820            .into_iter()
1821            .filter_map(|entity_id| state.live_by_id.get(entity_id))
1822            .map(|entry| Arc::clone(&entry.entity))
1823            .collect()
1824    }
1825
1826    #[must_use]
1827    /// Returns the number of live indexed entities.
1828    pub fn count(&self) -> usize {
1829        self.state.read().live_by_id.len()
1830    }
1831
1832    /// Ticks live entities currently in the ticking visibility set.
1833    pub fn tick_entities(&self, _tick_count: i32, runs_normally: bool) -> FxHashSet<ChunkPos> {
1834        let mut dirty_chunks = FxHashSet::default();
1835        let tick_candidates = self.ticking_entities_snapshot();
1836        for entity in tick_candidates {
1837            let Some(tick_context) = self.tick_context(entity.id()) else {
1838                continue;
1839            };
1840
1841            if entity.is_removed() {
1842                continue;
1843            }
1844
1845            if Self::is_entity_frozen_by_tick_rate(entity.as_ref(), runs_normally) {
1846                continue;
1847            }
1848
1849            let entity_chunk = tick_context.manager_chunk;
1850            entity.check_despawn();
1851            if entity.is_removed() {
1852                if let Some(chunk) = entity_chunk {
1853                    dirty_chunks.insert(chunk);
1854                }
1855                continue;
1856            }
1857
1858            if Self::is_valid_passenger_or_stop_riding(&entity) {
1859                continue;
1860            }
1861
1862            // Vanilla deliberately has no same-tick de-duplication here. A relationship change
1863            // may make an entity tick once as a passenger and once from its later root-list slot.
1864            self.tick_non_passenger(&entity, &mut dirty_chunks);
1865        }
1866        dirty_chunks
1867    }
1868
1869    fn ticking_entities_snapshot(&self) -> Vec<SharedEntity> {
1870        self.state.read().tick_list.snapshot()
1871    }
1872
1873    fn live_manager_owned_entity_chunk(&self, entity_id: i32) -> Option<ChunkPos> {
1874        self.state
1875            .read()
1876            .live_by_id
1877            .get(entity_id)
1878            .filter(|entry| entry.ownership == EntityOwnership::ManagerOwned)
1879            .map(|entry| entry.chunk)
1880    }
1881
1882    fn chunk_visibility(state: &ManagerState, chunk: ChunkPos) -> EntityVisibility {
1883        state
1884            .chunk_visibility
1885            .get(&chunk)
1886            .copied()
1887            .unwrap_or(EntityVisibility::Hidden)
1888    }
1889
1890    fn effective_visibility(
1891        entry: &EntityEntry,
1892        chunk_visibility: EntityVisibility,
1893    ) -> EntityVisibility {
1894        if entry.entity.is_always_ticking() {
1895            return EntityVisibility::Ticking;
1896        }
1897        if entry.ownership == EntityOwnership::External {
1898            return EntityVisibility::Tracked;
1899        }
1900        chunk_visibility
1901    }
1902
1903    fn lifecycle_visibility_for(
1904        entry: &EntityEntry,
1905        chunk_visibility: EntityVisibility,
1906    ) -> EntityVisibility {
1907        Self::effective_visibility(entry, chunk_visibility)
1908    }
1909
1910    fn apply_entity_lifecycle_after_insert(
1911        state: &mut ManagerState,
1912        entity_id: i32,
1913    ) -> EntityLifecycleChanges {
1914        let Some(entry) = state.live_by_id.get(entity_id) else {
1915            return EntityLifecycleChanges::default();
1916        };
1917        let visibility =
1918            Self::lifecycle_visibility_for(entry, Self::chunk_visibility(state, entry.chunk));
1919        let entity = Arc::clone(&entry.entity);
1920        let should_tick = visibility.is_ticking();
1921
1922        let mut lifecycle = EntityLifecycleChanges::default();
1923        if visibility.is_accessible() {
1924            lifecycle.tracking_started.push(Arc::clone(&entity));
1925        }
1926        if should_tick && state.tick_list.add(&entity) {
1927            lifecycle.ticking_started.push(entity);
1928        }
1929        lifecycle
1930    }
1931
1932    fn apply_chunk_visibility_change(
1933        state: &mut ManagerState,
1934        chunk: ChunkPos,
1935        previous: EntityVisibility,
1936        new: EntityVisibility,
1937    ) -> EntityLifecycleChanges {
1938        let entity_ids = Self::entity_ids_in_chunk_order(state, chunk);
1939        let mut lifecycle = EntityLifecycleChanges::default();
1940
1941        for entity_id in entity_ids {
1942            let Some(entry) = state.live_by_id.get_mut(entity_id) else {
1943                continue;
1944            };
1945
1946            entry.accessible = entry.ownership == EntityOwnership::External || new.is_accessible();
1947
1948            if entry.ownership != EntityOwnership::ManagerOwned {
1949                continue;
1950            }
1951
1952            let old_visibility = Self::lifecycle_visibility_for(entry, previous);
1953            let new_visibility = Self::lifecycle_visibility_for(entry, new);
1954            if old_visibility == new_visibility {
1955                continue;
1956            }
1957
1958            let entity = Arc::clone(&entry.entity);
1959            if old_visibility.is_ticking()
1960                && !new_visibility.is_ticking()
1961                && state.tick_list.remove(entity_id).is_some()
1962            {
1963                lifecycle.ticking_stopped.push(Arc::clone(&entity));
1964            }
1965
1966            if old_visibility.is_accessible() && !new_visibility.is_accessible() {
1967                state.accessible_order.remove(entity_id);
1968                lifecycle.tracking_stopped.push(Arc::clone(&entity));
1969            } else if !old_visibility.is_accessible() && new_visibility.is_accessible() {
1970                state.accessible_order.insert(entity_id);
1971                lifecycle.tracking_started.push(Arc::clone(&entity));
1972            }
1973
1974            if !old_visibility.is_ticking()
1975                && new_visibility.is_ticking()
1976                && state.tick_list.add(&entity)
1977            {
1978                lifecycle.ticking_started.push(entity);
1979            }
1980        }
1981
1982        lifecycle
1983    }
1984
1985    fn is_entity_frozen_by_tick_rate(entity: &dyn Entity, runs_normally: bool) -> bool {
1986        !runs_normally
1987            && entity.entity_type() != &vanilla_entities::PLAYER
1988            && entity.count_player_passengers() == 0
1989    }
1990
1991    fn has_pending_world_change_in_vehicle_chain(entity: &SharedEntity) -> bool {
1992        if entity.is_world_change_pending() {
1993            return true;
1994        }
1995
1996        let Some(mut vehicle) = entity.vehicle() else {
1997            return false;
1998        };
1999
2000        let mut visited = VehicleChain::new();
2001        visited.push(entity.id());
2002        loop {
2003            assert!(
2004                !visited.contains(&vehicle.id()),
2005                "cyclic passenger relationship involving entity {}",
2006                entity.id()
2007            );
2008            visited.push(vehicle.id());
2009            if vehicle.is_world_change_pending() {
2010                return true;
2011            }
2012            let Some(current) = vehicle.vehicle() else {
2013                return false;
2014            };
2015            vehicle = current;
2016        }
2017    }
2018
2019    fn is_accessible(state: &ManagerState, entry: &EntityEntry) -> bool {
2020        Self::is_accessible_at(state, entry.ownership, entry.chunk)
2021    }
2022
2023    fn is_accessible_at(state: &ManagerState, ownership: EntityOwnership, chunk: ChunkPos) -> bool {
2024        ownership == EntityOwnership::External
2025            || Self::chunk_visibility(state, chunk).is_accessible()
2026    }
2027
2028    fn is_valid_passenger_or_stop_riding(entity: &SharedEntity) -> bool {
2029        let Some(vehicle) = entity.vehicle() else {
2030            return false;
2031        };
2032
2033        if !vehicle.is_removed() && vehicle.has_passenger(entity.as_ref()) {
2034            Self::assert_acyclic_vehicle_chain(entity);
2035            return true;
2036        }
2037
2038        entity.stop_riding();
2039        false
2040    }
2041
2042    fn assert_acyclic_vehicle_chain(entity: &SharedEntity) {
2043        let mut visited = VehicleChain::new();
2044        visited.push(entity.id());
2045
2046        let mut vehicle = entity.vehicle();
2047        while let Some(current) = vehicle {
2048            assert!(
2049                !visited.contains(&current.id()),
2050                "cyclic passenger relationship involving entity {}",
2051                entity.id()
2052            );
2053            visited.push(current.id());
2054            vehicle = current.vehicle();
2055        }
2056    }
2057
2058    fn tick_non_passenger(&self, entity: &SharedEntity, dirty_chunks: &mut FxHashSet<ChunkPos>) {
2059        snapshot_old_pos_and_rot_for_tick(entity.as_ref());
2060        entity.advance_tick_count();
2061        Arc::clone(entity).tick();
2062        self.mark_dirty_after_tick(entity, dirty_chunks);
2063        self.tick_vehicle_passengers(entity.as_ref(), dirty_chunks);
2064    }
2065
2066    fn tick_vehicle_passengers(
2067        &self,
2068        vehicle: &dyn Entity,
2069        dirty_chunks: &mut FxHashSet<ChunkPos>,
2070    ) {
2071        let mut post_tick = |entity: &SharedEntity| {
2072            self.mark_dirty_after_tick(entity, dirty_chunks);
2073        };
2074        tick_vehicle_passengers_if(vehicle, &mut post_tick, &mut |entity| {
2075            self.can_tick_entity_now(entity.id())
2076        });
2077    }
2078
2079    fn mark_dirty_after_tick(&self, entity: &SharedEntity, dirty_chunks: &mut FxHashSet<ChunkPos>) {
2080        if self.live_manager_owned_entity_chunk(entity.id()).is_some() {
2081            dirty_chunks.insert(ChunkPos::from_entity_pos(entity.position()));
2082        }
2083    }
2084
2085    fn can_tick_entity_now(&self, entity_id: i32) -> bool {
2086        self.tick_context(entity_id).is_some()
2087    }
2088
2089    fn tick_context(&self, entity_id: i32) -> Option<TickContext> {
2090        let state = self.state.read();
2091        let entry = state.live_by_id.get(entity_id)?;
2092
2093        if Self::has_pending_world_change_in_vehicle_chain(&entry.entity) {
2094            return None;
2095        }
2096
2097        let in_tick_list = state.tick_list.contains(entity_id);
2098        let can_tick = match entry.ownership {
2099            EntityOwnership::External => {
2100                entry.entity.entity_type() == &vanilla_entities::PLAYER || in_tick_list
2101            }
2102            EntityOwnership::ManagerOwned => in_tick_list,
2103        };
2104        can_tick.then_some(TickContext {
2105            manager_chunk: (entry.ownership == EntityOwnership::ManagerOwned)
2106                .then_some(entry.chunk),
2107        })
2108    }
2109
2110    fn insert_live_entry(state: &mut ManagerState, mut entry: EntityEntry) {
2111        let entity_id = entry.entity.id();
2112        entry.bounding_box = entry.entity.bounding_box();
2113        entry.spatial_cells = EntitySpatialCellBounds::from_aabb(&entry.bounding_box).cells();
2114        let is_accessible = Self::is_accessible_at(state, entry.ownership, entry.chunk);
2115        entry.accessible = is_accessible;
2116        assert!(
2117            !state.live_by_id.contains_key(entity_id),
2118            "entity id {entity_id} is already registered in the world entity manager"
2119        );
2120        assert!(
2121            state.live_by_uuid.insert(entry.uuid, entity_id).is_none(),
2122            "entity uuid {} is already registered in the world entity manager",
2123            entry.uuid
2124        );
2125        entry.query_order = EntityQueryOrder {
2126            section: PackedSectionPos::from(entry.section),
2127            insertion: Self::next_section_order(state),
2128        };
2129
2130        state
2131            .by_section
2132            .entry(PackedSectionPos::from(entry.section))
2133            .or_default()
2134            .insert(entity_id);
2135        state
2136            .by_chunk
2137            .entry(entry.chunk)
2138            .or_default()
2139            .insert(entity_id);
2140        let hard_collision_relevant = entry.hard_collision_relevant;
2141        let entity_slot = state.live_by_id.insert(entity_id, entry);
2142        Self::insert_into_spatial_indexes(state, entity_slot, hard_collision_relevant, None);
2143
2144        if is_accessible {
2145            state.accessible_order.insert(entity_id);
2146        }
2147    }
2148
2149    fn contains_uuid(state: &ManagerState, uuid: Uuid) -> bool {
2150        state.live_by_uuid.contains_key(&uuid)
2151            || state
2152                .unloading_by_chunk
2153                .values()
2154                .flatten()
2155                .any(|entry| entry.uuid == uuid)
2156            || state
2157                .save_pending_by_chunk
2158                .values()
2159                .flatten()
2160                .any(|entry| entry.uuid == uuid)
2161    }
2162
2163    fn contains_id(state: &ManagerState, entity_id: i32) -> bool {
2164        state.live_by_id.contains_key(entity_id)
2165            || state
2166                .unloading_by_chunk
2167                .values()
2168                .flatten()
2169                .any(|entry| entry.entity.id() == entity_id)
2170            || state
2171                .save_pending_by_chunk
2172                .values()
2173                .flatten()
2174                .any(|entry| entry.entity.id() == entity_id)
2175    }
2176
2177    fn push_saveable_entity(
2178        result: &mut Vec<SharedEntity>,
2179        seen_ids: &mut FxHashSet<i32>,
2180        seen_uuids: &mut FxHashSet<Uuid>,
2181        entry: &EntityEntry,
2182    ) {
2183        if !entry.should_save() || !seen_ids.insert(entry.entity.id()) {
2184            return;
2185        }
2186        assert!(
2187            seen_uuids.insert(entry.uuid),
2188            "duplicate saveable entity uuid {} in world entity manager",
2189            entry.uuid
2190        );
2191        result.push(Arc::clone(&entry.entity));
2192    }
2193
2194    fn push_unsaved_entity_report(
2195        saved_chunks: &FxHashSet<ChunkPos>,
2196        seen: &mut FxHashSet<i32>,
2197        reports: &mut Vec<UnsavedEntityReport>,
2198        entry: &EntityEntry,
2199    ) {
2200        if saved_chunks.contains(&entry.chunk)
2201            || !entry.should_save()
2202            || !seen.insert(entry.entity.id())
2203        {
2204            return;
2205        }
2206
2207        reports.push(UnsavedEntityReport {
2208            entity_id: entry.entity.id(),
2209            uuid: entry.uuid,
2210            chunk: entry.chunk,
2211        });
2212    }
2213
2214    fn remove_live_entry(state: &mut ManagerState, entity_id: i32) -> Option<EntityEntry> {
2215        let (entity_slot, entry) = state.live_by_id.remove(entity_id)?;
2216        state.tick_list.remove(entity_id);
2217        state.live_by_uuid.remove(&entry.uuid);
2218        state.accessible_order.remove(entity_id);
2219        Self::remove_from_section(state, entry.section, entity_id);
2220        Self::remove_from_spatial_indexes(
2221            state,
2222            &entry.spatial_cells,
2223            entity_slot,
2224            entry.hard_collision_relevant,
2225            None,
2226        );
2227
2228        Self::remove_from_chunk(state, entry.chunk, entity_id);
2229
2230        Some(entry)
2231    }
2232
2233    fn next_section_order(state: &mut ManagerState) -> u64 {
2234        assert!(
2235            state.next_section_order < u64::MAX,
2236            "entity section insertion order exhausted"
2237        );
2238
2239        let order = state.next_section_order;
2240        state.next_section_order += 1;
2241
2242        order
2243    }
2244
2245    /// Indexes `entity_slot` in every spatial index it belongs to, skipping cells inside `retained`.
2246    fn insert_into_spatial_indexes(
2247        state: &mut ManagerState,
2248        entity_slot: EntitySlot,
2249        hard_collision_relevant: bool,
2250        retained: Option<EntitySpatialCellBounds>,
2251    ) {
2252        Self::insert_into_spatial_cells(
2253            state,
2254            entity_slot,
2255            EntityCollisionCandidates::All,
2256            retained,
2257        );
2258
2259        if hard_collision_relevant {
2260            Self::insert_into_spatial_cells(
2261                state,
2262                entity_slot,
2263                EntityCollisionCandidates::HardCollisionRelevant,
2264                retained,
2265            );
2266        }
2267    }
2268
2269    /// Removes `entity_slot` from every spatial index it belongs to, keeping cells inside `retained`.
2270    fn remove_from_spatial_indexes(
2271        state: &mut ManagerState,
2272        cells: &[EntitySpatialCell],
2273        entity_slot: EntitySlot,
2274        hard_collision_relevant: bool,
2275        retained: Option<EntitySpatialCellBounds>,
2276    ) {
2277        Self::remove_from_spatial_cells(
2278            state,
2279            cells,
2280            entity_slot,
2281            EntityCollisionCandidates::All,
2282            retained,
2283        );
2284
2285        if hard_collision_relevant {
2286            Self::remove_from_spatial_cells(
2287                state,
2288                cells,
2289                entity_slot,
2290                EntityCollisionCandidates::HardCollisionRelevant,
2291                retained,
2292            );
2293        }
2294    }
2295
2296    fn insert_into_spatial_cells(
2297        state: &mut ManagerState,
2298        entity_slot: EntitySlot,
2299        candidates: EntityCollisionCandidates,
2300        retained_bounds: Option<EntitySpatialCellBounds>,
2301    ) {
2302        match retained_bounds {
2303            Some(bounds) => {
2304                Self::insert_into_spatial_cells_matching(state, entity_slot, candidates, |cell| {
2305                    !bounds.contains(cell)
2306                });
2307            }
2308            None => {
2309                Self::insert_into_spatial_cells_matching(state, entity_slot, candidates, |_| true);
2310            }
2311        }
2312    }
2313
2314    fn insert_into_spatial_cells_matching(
2315        state: &mut ManagerState,
2316        entity_slot: EntitySlot,
2317        candidates: EntityCollisionCandidates,
2318        accept_cell: impl Fn(EntitySpatialCell) -> bool,
2319    ) {
2320        let entry = state.live_by_id.entry(entity_slot);
2321        let query_order = entry.query_order;
2322        let spatial_index = match candidates {
2323            EntityCollisionCandidates::All => &mut state.by_spatial_cell,
2324            EntityCollisionCandidates::HardCollisionRelevant => &mut state.by_hard_collision_cell,
2325        };
2326
2327        for cell in &entry.spatial_cells {
2328            if !accept_cell(*cell) {
2329                continue;
2330            }
2331
2332            let entries = spatial_index.entry(*cell).or_default();
2333            let insertion_index = entries
2334                .entities
2335                .partition_point(|slot| state.live_by_id.entry(*slot).query_order < query_order);
2336            entries.entities.insert(insertion_index, entity_slot);
2337        }
2338    }
2339
2340    fn remove_from_spatial_cells(
2341        state: &mut ManagerState,
2342        cells: &[EntitySpatialCell],
2343        entity_slot: EntitySlot,
2344        candidates: EntityCollisionCandidates,
2345        retained_bounds: Option<EntitySpatialCellBounds>,
2346    ) {
2347        match retained_bounds {
2348            Some(bounds) => Self::remove_from_spatial_cells_matching(
2349                state,
2350                cells,
2351                entity_slot,
2352                candidates,
2353                |cell| !bounds.contains(cell),
2354            ),
2355            None => Self::remove_from_spatial_cells_matching(
2356                state,
2357                cells,
2358                entity_slot,
2359                candidates,
2360                |_| true,
2361            ),
2362        }
2363    }
2364
2365    fn remove_from_spatial_cells_matching(
2366        state: &mut ManagerState,
2367        cells: &[EntitySpatialCell],
2368        entity_slot: EntitySlot,
2369        candidates: EntityCollisionCandidates,
2370        accept_cell: impl Fn(EntitySpatialCell) -> bool,
2371    ) {
2372        let spatial_index = match candidates {
2373            EntityCollisionCandidates::All => &mut state.by_spatial_cell,
2374            EntityCollisionCandidates::HardCollisionRelevant => &mut state.by_hard_collision_cell,
2375        };
2376
2377        for cell in cells {
2378            if !accept_cell(*cell) {
2379                continue;
2380            }
2381
2382            let remove_cell = if let Some(entries) = spatial_index.get_mut(cell) {
2383                if let Some(index) = entries
2384                    .entities
2385                    .iter()
2386                    .position(|slot| *slot == entity_slot)
2387                {
2388                    entries.entities.remove(index);
2389                }
2390
2391                entries.entities.is_empty()
2392            } else {
2393                false
2394            };
2395
2396            if remove_cell {
2397                spatial_index.remove(cell);
2398            }
2399        }
2400    }
2401
2402    fn remove_from_section(state: &mut ManagerState, section: SectionPos, entity_id: i32) {
2403        let packed = PackedSectionPos::from(section);
2404        let remove_section = if let Some(entity_ids) = state.by_section.get_mut(&packed) {
2405            entity_ids.remove(entity_id);
2406            entity_ids.is_empty()
2407        } else {
2408            false
2409        };
2410        if remove_section {
2411            state.by_section.remove(&packed);
2412        }
2413    }
2414
2415    fn remove_from_chunk(state: &mut ManagerState, chunk: ChunkPos, entity_id: i32) {
2416        let remove_chunk = if let Some(entity_ids) = state.by_chunk.get_mut(&chunk) {
2417            entity_ids.remove(&entity_id);
2418            entity_ids.is_empty()
2419        } else {
2420            false
2421        };
2422        if remove_chunk {
2423            state.by_chunk.remove(&chunk);
2424        }
2425    }
2426}
2427
2428impl Default for WorldEntityManager {
2429    fn default() -> Self {
2430        Self::new()
2431    }
2432}
2433
2434mod entries;
2435mod spatial_query;
2436
2437#[cfg(feature = "benchmark-support")]
2438pub mod benchmark_support;
2439
2440#[cfg(test)]
2441mod tests;