1use 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
23const ENTITY_SPATIAL_CELL_SIZE: f64 = 4.0;
25const ENTITY_SPATIAL_CELL_OFFSET: f64 = ENTITY_SPATIAL_CELL_SIZE / 2.0;
27const 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#[derive(Debug, Clone, PartialEq, Eq)]
98pub enum AddEntityError {
99 ChunkNotLoaded {
101 entity_id: i32,
103 chunk: ChunkPos,
105 },
106 DuplicateUuid {
108 entity_id: i32,
110 uuid: Uuid,
112 },
113 RemovedEntity {
115 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#[derive(Debug, Clone, PartialEq, Eq)]
140pub enum EntityMoveError {
141 NotLive {
143 entity_id: i32,
145 },
146 Inactive {
148 entity_id: i32,
150 },
151 UnloadedDestination {
153 entity_id: i32,
155 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183pub enum EntityOwnership {
184 ManagerOwned,
186 External,
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
196pub enum EntityVisibility {
197 Hidden,
199 Tracked,
201 Ticking,
203}
204
205impl EntityVisibility {
206 #[must_use]
208 pub const fn is_accessible(self) -> bool {
209 matches!(self, Self::Tracked | Self::Ticking)
210 }
211
212 #[must_use]
214 pub const fn is_ticking(self) -> bool {
215 matches!(self, Self::Ticking)
216 }
217}
218
219#[derive(Default)]
221pub struct EntityLifecycleChanges {
222 pub tracking_started: Vec<SharedEntity>,
224 pub tracking_stopped: Vec<SharedEntity>,
226 pub ticking_started: Vec<SharedEntity>,
228 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#[derive(Debug, Clone)]
254pub struct EntityMoveUpdate {
255 pub entity_id: i32,
257 pub old_section: SectionPos,
259 pub new_section: SectionPos,
261 pub old_chunk: ChunkPos,
263 pub new_chunk: ChunkPos,
265 pub old_accessible: bool,
267 pub new_accessible: bool,
269 pub old_ticking: bool,
271 pub new_ticking: bool,
273}
274
275impl EntityMoveUpdate {
276 #[must_use]
278 pub fn section_changed(&self) -> bool {
279 self.old_section != self.new_section
280 }
281
282 #[must_use]
284 pub fn chunk_changed(&self) -> bool {
285 self.old_chunk != self.new_chunk
286 }
287
288 #[must_use]
290 pub const fn accessibility_changed(&self) -> bool {
291 self.old_accessible != self.new_accessible
292 }
293
294 #[must_use]
296 pub const fn became_accessible(&self) -> bool {
297 !self.old_accessible && self.new_accessible
298 }
299
300 #[must_use]
302 pub const fn became_inaccessible(&self) -> bool {
303 self.old_accessible && !self.new_accessible
304 }
305
306 #[must_use]
308 pub const fn became_ticking(&self) -> bool {
309 !self.old_ticking && self.new_ticking
310 }
311
312 #[must_use]
314 pub const fn became_non_ticking(&self) -> bool {
315 self.old_ticking && !self.new_ticking
316 }
317}
318
319#[derive(Debug, Clone, PartialEq, Eq)]
321pub struct UnsavedEntityReport {
322 pub entity_id: i32,
324 pub uuid: Uuid,
326 pub chunk: ChunkPos,
328}
329
330#[derive(Default)]
332pub struct ChunkEntityLoadResult {
333 pub restored: Vec<SharedEntity>,
335 pub tracking_started: Vec<SharedEntity>,
337 pub ticking_started: Vec<SharedEntity>,
339 pub needs_save: bool,
341}
342
343#[derive(Default)]
345pub struct ChunkEntityUnloadStart {
346 pub retained: Vec<SharedEntity>,
348 pub tracking_stopped: Vec<SharedEntity>,
350 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
490pub 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 #[must_use]
509 pub fn new() -> Self {
510 Self {
511 state: SyncRwLock::new(ManagerState::default()),
512 }
513 }
514
515 #[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 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 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 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 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 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 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 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 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 #[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 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 #[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(¤t.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 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 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 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 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 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 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 #[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 #[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 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 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 #[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 #[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 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 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 pub fn count(&self) -> usize {
1403 self.state.read().live_by_id.len()
1404 }
1405
1406 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;