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::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}