steel_core/entity/
storage.rs1use std::{collections::hash_map::Entry, fmt, mem};
7
8use rustc_hash::FxHashMap;
9use steel_utils::locks::SyncRwLock;
10
11use super::{RemovalReason, SharedEntity};
12
13pub(crate) struct EntityStorage {
18 state: SyncRwLock<EntityStorageState>,
19}
20
21enum EntityStorageState {
22 Open(FxHashMap<i32, SharedEntity>),
23 Closed,
24}
25
26#[must_use]
28pub(crate) enum EntityStorageAddResult {
29 Staged,
31 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 #[must_use]
53 pub(crate) fn new() -> Self {
54 Self {
55 state: SyncRwLock::new(EntityStorageState::Open(FxHashMap::default())),
56 }
57 }
58
59 #[must_use]
61 pub(crate) const fn new_closed() -> Self {
62 Self {
63 state: SyncRwLock::new(EntityStorageState::Closed),
64 }
65 }
66
67 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 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 #[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 #[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 #[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::test_support::TestEntity;
160
161 fn test_item(id: i32) -> SharedEntity {
162 TestEntity::shared(id, DVec3::ZERO, Weak::new(), &vanilla_entities::ITEM)
163 }
164
165 #[test]
166 fn saveable_entities_keep_unloaded_to_chunk_removals() {
167 let storage = EntityStorage::new();
168 let unloaded = test_item(1);
169 let discarded = test_item(2);
170
171 unloaded.set_removed(RemovalReason::UnloadedToChunk);
172 discarded.set_removed(RemovalReason::Discarded);
173 assert!(matches!(
174 storage.add(unloaded),
175 EntityStorageAddResult::Staged
176 ));
177 assert!(matches!(
178 storage.add(discarded),
179 EntityStorageAddResult::Staged
180 ));
181
182 let saveable = storage.get_saveable_entities();
183
184 assert_eq!(saveable.len(), 1);
185 assert_eq!(saveable[0].id(), 1);
186 }
187
188 #[test]
189 #[should_panic(expected = "already present in proto entity storage")]
190 fn add_rejects_duplicate_entity_ids() {
191 let storage = EntityStorage::new();
192
193 assert!(matches!(
194 storage.add(test_item(1)),
195 EntityStorageAddResult::Staged
196 ));
197 let _ = storage.add(test_item(1));
198 }
199
200 #[test]
201 fn close_drains_staged_entities_and_returns_late_adds() {
202 let storage = EntityStorage::new();
203 let staged = test_item(1);
204 assert!(matches!(
205 storage.add(Arc::clone(&staged)),
206 EntityStorageAddResult::Staged
207 ));
208
209 let drained = storage.close_and_drain();
210 assert_eq!(drained.len(), 1);
211 assert!(Arc::ptr_eq(&drained[0], &staged));
212 assert!(storage.get_all().is_empty());
213 assert!(storage.get_saveable_entities().is_empty());
214
215 let late = test_item(2);
216 let EntityStorageAddResult::Closed(returned) = storage.add(Arc::clone(&late)) else {
217 panic!("closed storage must return ownership of a late entity");
218 };
219 assert!(Arc::ptr_eq(&returned, &late));
220 assert!(storage.close_and_drain().is_empty());
221 }
222
223 #[test]
224 fn concurrent_add_and_close_leave_entity_with_exactly_one_owner() {
225 for id in 0..64 {
226 let storage = Arc::new(EntityStorage::new());
227 let barrier = Arc::new(Barrier::new(2));
228 let entity = test_item(id);
229 let add_storage = Arc::clone(&storage);
230 let add_barrier = Arc::clone(&barrier);
231 let add_entity = Arc::clone(&entity);
232 let add_thread = thread::spawn(move || {
233 add_barrier.wait();
234 add_storage.add(add_entity)
235 });
236
237 barrier.wait();
238 let drained = storage.close_and_drain();
239 let Ok(add_result) = add_thread.join() else {
240 panic!("entity staging thread panicked");
241 };
242
243 match add_result {
244 EntityStorageAddResult::Staged => {
245 assert_eq!(drained.len(), 1);
246 assert!(Arc::ptr_eq(&drained[0], &entity));
247 }
248 EntityStorageAddResult::Closed(returned) => {
249 assert!(drained.is_empty());
250 assert!(Arc::ptr_eq(&returned, &entity));
251 }
252 }
253 assert!(storage.get_all().is_empty());
254 }
255 }
256}