Skip to main content

steel_core/entity/
storage.rs

1//! Proto-chunk entity storage.
2//!
3//! Full chunks do not own or tick entities. `EntityStorage` only keeps entities
4//! staged in proto chunks until promotion hands them to `WorldEntityManager`.
5
6use std::{collections::hash_map::Entry, fmt, mem};
7
8use rustc_hash::FxHashMap;
9use steel_utils::locks::SyncRwLock;
10
11use super::{RemovalReason, SharedEntity};
12
13/// Storage for entities staged in a proto chunk.
14///
15/// Steel keeps proto entity staging separate from full-chunk runtime ownership:
16/// promoted or loaded full-chunk entities are owned and ticked by `WorldEntityManager`.
17pub(crate) struct EntityStorage {
18    state: SyncRwLock<EntityStorageState>,
19}
20
21enum EntityStorageState {
22    Open(FxHashMap<i32, SharedEntity>),
23    Closed,
24}
25
26/// Result of trying to stage an entity before full-chunk promotion.
27#[must_use]
28pub(crate) enum EntityStorageAddResult {
29    /// The entity was staged in proto-chunk storage.
30    Staged,
31    /// Promotion already closed storage, so the caller retains the entity.
32    Closed(SharedEntity),
33}
34
35fn should_keep_for_save(entity: &SharedEntity) -> bool {
36    !entity.is_removed()
37        || entity
38            .removal_reason()
39            .is_some_and(RemovalReason::should_save)
40}
41
42impl fmt::Debug for EntityStorage {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        f.debug_struct("EntityStorage")
45            .field("len", &self.len())
46            .finish()
47    }
48}
49
50impl EntityStorage {
51    /// Creates a new empty entity storage.
52    #[must_use]
53    pub(crate) fn new() -> Self {
54        Self {
55            state: SyncRwLock::new(EntityStorageState::Open(FxHashMap::default())),
56        }
57    }
58
59    /// Creates empty storage that has already crossed the Full promotion boundary.
60    #[must_use]
61    pub(crate) const fn new_closed() -> Self {
62        Self {
63            state: SyncRwLock::new(EntityStorageState::Closed),
64        }
65    }
66
67    /// Tries to add an entity to proto storage.
68    ///
69    /// This operation linearizes with [`Self::close_and_drain`]. If promotion
70    /// closes storage first, ownership is returned so the caller can apply its
71    /// phase-specific disposition.
72    pub(crate) fn add(&self, entity: SharedEntity) -> EntityStorageAddResult {
73        let id = entity.id();
74        let mut state = self.state.write();
75        let EntityStorageState::Open(entities) = &mut *state else {
76            return EntityStorageAddResult::Closed(entity);
77        };
78        match entities.entry(id) {
79            Entry::Vacant(entry) => {
80                entry.insert(entity);
81                EntityStorageAddResult::Staged
82            }
83            Entry::Occupied(_) => {
84                panic!("entity id {id} is already present in proto entity storage")
85            }
86        }
87    }
88
89    /// Atomically closes proto storage and drains every staged entity.
90    ///
91    /// Later adds return [`EntityStorageAddResult::Closed`]. Repeated closes
92    /// return an empty collection.
93    pub(crate) fn close_and_drain(&self) -> Vec<SharedEntity> {
94        let mut state = self.state.write();
95        let EntityStorageState::Open(entities) =
96            mem::replace(&mut *state, EntityStorageState::Closed)
97        else {
98            return Vec::new();
99        };
100        entities.into_values().collect()
101    }
102
103    /// Returns all staged entities.
104    #[must_use]
105    pub(crate) fn get_all(&self) -> Vec<SharedEntity> {
106        let state = self.state.read();
107        let EntityStorageState::Open(entities) = &*state else {
108            return Vec::new();
109        };
110        entities.values().cloned().collect()
111    }
112
113    /// Returns the number of staged entities.
114    #[must_use]
115    pub(crate) fn len(&self) -> usize {
116        let state = self.state.read();
117        match &*state {
118            EntityStorageState::Open(entities) => entities.len(),
119            EntityStorageState::Closed => 0,
120        }
121    }
122
123    /// Returns staged entities that should be saved when the proto chunk is persisted.
124    ///
125    /// Excludes:
126    /// - Removed entities
127    /// - Entity types with `can_serialize = false` (including players)
128    #[must_use]
129    pub(crate) fn get_saveable_entities(&self) -> Vec<SharedEntity> {
130        let state = self.state.read();
131        let EntityStorageState::Open(entities) = &*state else {
132            return Vec::new();
133        };
134        entities
135            .values()
136            .filter(|e| should_keep_for_save(e) && e.entity_type().can_serialize)
137            .cloned()
138            .collect()
139    }
140}
141
142impl Default for EntityStorage {
143    fn default() -> Self {
144        Self::new()
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use std::{
151        sync::{Arc, Barrier, Weak},
152        thread,
153    };
154
155    use glam::DVec3;
156    use steel_registry::vanilla_entities;
157
158    use super::*;
159    use crate::entity::entities::RawEntity;
160
161    fn raw_item(id: i32) -> SharedEntity {
162        Arc::new(RawEntity::new(
163            id,
164            DVec3::ZERO,
165            Weak::new(),
166            &vanilla_entities::ITEM,
167        ))
168    }
169
170    #[test]
171    fn saveable_entities_keep_unloaded_to_chunk_removals() {
172        let storage = EntityStorage::new();
173        let unloaded = raw_item(1);
174        let discarded = raw_item(2);
175
176        unloaded.set_removed(RemovalReason::UnloadedToChunk);
177        discarded.set_removed(RemovalReason::Discarded);
178        assert!(matches!(
179            storage.add(unloaded),
180            EntityStorageAddResult::Staged
181        ));
182        assert!(matches!(
183            storage.add(discarded),
184            EntityStorageAddResult::Staged
185        ));
186
187        let saveable = storage.get_saveable_entities();
188
189        assert_eq!(saveable.len(), 1);
190        assert_eq!(saveable[0].id(), 1);
191    }
192
193    #[test]
194    #[should_panic(expected = "already present in proto entity storage")]
195    fn add_rejects_duplicate_entity_ids() {
196        let storage = EntityStorage::new();
197
198        assert!(matches!(
199            storage.add(raw_item(1)),
200            EntityStorageAddResult::Staged
201        ));
202        let _ = storage.add(raw_item(1));
203    }
204
205    #[test]
206    fn close_drains_staged_entities_and_returns_late_adds() {
207        let storage = EntityStorage::new();
208        let staged = raw_item(1);
209        assert!(matches!(
210            storage.add(Arc::clone(&staged)),
211            EntityStorageAddResult::Staged
212        ));
213
214        let drained = storage.close_and_drain();
215        assert_eq!(drained.len(), 1);
216        assert!(Arc::ptr_eq(&drained[0], &staged));
217        assert!(storage.get_all().is_empty());
218        assert!(storage.get_saveable_entities().is_empty());
219
220        let late = raw_item(2);
221        let EntityStorageAddResult::Closed(returned) = storage.add(Arc::clone(&late)) else {
222            panic!("closed storage must return ownership of a late entity");
223        };
224        assert!(Arc::ptr_eq(&returned, &late));
225        assert!(storage.close_and_drain().is_empty());
226    }
227
228    #[test]
229    fn concurrent_add_and_close_leave_entity_with_exactly_one_owner() {
230        for id in 0..64 {
231            let storage = Arc::new(EntityStorage::new());
232            let barrier = Arc::new(Barrier::new(2));
233            let entity = raw_item(id);
234            let add_storage = Arc::clone(&storage);
235            let add_barrier = Arc::clone(&barrier);
236            let add_entity = Arc::clone(&entity);
237            let add_thread = thread::spawn(move || {
238                add_barrier.wait();
239                add_storage.add(add_entity)
240            });
241
242            barrier.wait();
243            let drained = storage.close_and_drain();
244            let Ok(add_result) = add_thread.join() else {
245                panic!("entity staging thread panicked");
246            };
247
248            match add_result {
249                EntityStorageAddResult::Staged => {
250                    assert_eq!(drained.len(), 1);
251                    assert!(Arc::ptr_eq(&drained[0], &entity));
252                }
253                EntityStorageAddResult::Closed(returned) => {
254                    assert!(drained.is_empty());
255                    assert!(Arc::ptr_eq(&returned, &entity));
256                }
257            }
258            assert!(storage.get_all().is_empty());
259        }
260    }
261}