Skip to main content

galeon_engine/
entity.rs

1// SPDX-License-Identifier: AGPL-3.0-only OR Commercial
2
3use crate::archetype::EntityLocation;
4
5/// A lightweight entity identifier with generational indexing.
6///
7/// The generation field prevents use-after-despawn bugs: if an entity is
8/// despawned and its slot reused, the old `Entity` handle will fail
9/// `is_alive` checks because the generation won't match.
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
11pub struct Entity {
12    pub(crate) index: u32,
13    pub(crate) generation: u32,
14}
15
16impl Entity {
17    /// Reconstruct an `Entity` handle from its raw index and generation.
18    ///
19    /// This is intended for the WASM bridge where JS passes back an entity ID
20    /// that was previously returned by `spawn`. The caller is responsible for
21    /// providing a valid (index, generation) pair — passing stale or fabricated
22    /// values is safe but will cause `is_alive` / `get` to return `None`.
23    pub fn from_raw(index: u32, generation: u32) -> Self {
24        Self { index, generation }
25    }
26
27    /// Returns the index portion of this entity ID.
28    pub fn index(self) -> u32 {
29        self.index
30    }
31
32    /// Returns the generation portion of this entity ID.
33    pub fn generation(self) -> u32 {
34        self.generation
35    }
36}
37
38// ---------------------------------------------------------------------------
39// EntityMeta
40// ---------------------------------------------------------------------------
41
42/// Per-slot metadata for an entity: generation and archetype location.
43#[derive(Clone, Debug)]
44pub(crate) struct EntityMeta {
45    /// Current generation for this slot. Incremented on each dealloc.
46    pub generation: u32,
47    /// Whether this slot is alive.
48    pub alive: bool,
49    /// Where this entity lives in archetype storage. `None` until placed.
50    pub location: Option<EntityLocation>,
51}
52
53impl EntityMeta {
54    fn new_alive() -> Self {
55        Self {
56            generation: 0,
57            alive: true,
58            location: None,
59        }
60    }
61}
62
63// ---------------------------------------------------------------------------
64// EntityMetaStore
65// ---------------------------------------------------------------------------
66
67/// Manages entity allocation, generation tracking, and archetype location.
68///
69/// Replaces `EntityAllocator` with added location tracking for archetype
70/// storage. Maintains the same public contract for alloc/dealloc/is_alive.
71pub(crate) struct EntityMetaStore {
72    metas: Vec<EntityMeta>,
73    free: Vec<u32>,
74}
75
76impl EntityMetaStore {
77    pub fn new() -> Self {
78        Self {
79            metas: Vec::new(),
80            free: Vec::new(),
81        }
82    }
83
84    /// Allocate a new entity, reusing a freed slot if available.
85    pub fn alloc(&mut self) -> Entity {
86        if let Some(index) = self.free.pop() {
87            let meta = &mut self.metas[index as usize];
88            meta.alive = true;
89            meta.location = None;
90            Entity {
91                index,
92                generation: meta.generation,
93            }
94        } else {
95            let index = self.metas.len() as u32;
96            self.metas.push(EntityMeta::new_alive());
97            Entity {
98                index,
99                generation: 0,
100            }
101        }
102    }
103
104    /// Deallocate an entity. Returns `true` if it was alive.
105    pub fn dealloc(&mut self, entity: Entity) -> bool {
106        let idx = entity.index as usize;
107        if idx < self.metas.len() {
108            let meta = &mut self.metas[idx];
109            if meta.generation == entity.generation && meta.alive {
110                meta.alive = false;
111                meta.generation += 1;
112                meta.location = None;
113                self.free.push(entity.index);
114                return true;
115            }
116        }
117        false
118    }
119
120    /// Check whether an entity handle is still alive.
121    pub fn is_alive(&self, entity: Entity) -> bool {
122        let idx = entity.index as usize;
123        idx < self.metas.len()
124            && self.metas[idx].generation == entity.generation
125            && self.metas[idx].alive
126    }
127
128    /// Returns the total number of allocated slots (including dead).
129    #[allow(dead_code)]
130    pub fn len(&self) -> usize {
131        self.metas.len()
132    }
133
134    /// Set the archetype location for an entity.
135    pub fn set_location(&mut self, entity: Entity, location: EntityLocation) {
136        let idx = entity.index as usize;
137        debug_assert!(self.is_alive(entity), "set_location on dead entity");
138        self.metas[idx].location = Some(location);
139    }
140
141    /// Get the archetype location for an entity.
142    pub fn get_location(&self, entity: Entity) -> Option<EntityLocation> {
143        let idx = entity.index as usize;
144        if self.is_alive(entity) {
145            self.metas[idx].location
146        } else {
147            None
148        }
149    }
150
151    /// Returns an `Entity` handle for a given index, if it's alive.
152    #[allow(dead_code)]
153    pub fn entity_at(&self, index: u32) -> Option<Entity> {
154        let idx = index as usize;
155        if idx < self.metas.len() && self.metas[idx].alive {
156            Some(Entity {
157                index,
158                generation: self.metas[idx].generation,
159            })
160        } else {
161            None
162        }
163    }
164
165    /// Returns an iterator over all alive entity handles.
166    pub fn alive_entities(&self) -> impl Iterator<Item = Entity> + '_ {
167        self.metas
168            .iter()
169            .enumerate()
170            .filter(|(_, meta)| meta.alive)
171            .map(|(i, meta)| Entity {
172                index: i as u32,
173                generation: meta.generation,
174            })
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use crate::archetype::{ArchetypeId, EntityLocation};
182
183    // ---- EntityMetaStore --------------------------------------------------
184
185    #[test]
186    fn meta_store_alloc_returns_sequential_indices() {
187        let mut store = EntityMetaStore::new();
188        let e0 = store.alloc();
189        let e1 = store.alloc();
190        assert_eq!(e0.index, 0);
191        assert_eq!(e1.index, 1);
192        assert_eq!(e0.generation, 0);
193        assert_eq!(e1.generation, 0);
194    }
195
196    #[test]
197    fn meta_store_dealloc_and_reuse_bumps_generation() {
198        let mut store = EntityMetaStore::new();
199        let e0 = store.alloc();
200        assert!(store.dealloc(e0));
201
202        let e0_reused = store.alloc();
203        assert_eq!(e0_reused.index, 0);
204        assert_eq!(e0_reused.generation, 1);
205    }
206
207    #[test]
208    fn meta_store_is_alive_returns_false_after_dealloc() {
209        let mut store = EntityMetaStore::new();
210        let e = store.alloc();
211        assert!(store.is_alive(e));
212        store.dealloc(e);
213        assert!(!store.is_alive(e));
214    }
215
216    #[test]
217    fn meta_store_stale_handle_is_not_alive() {
218        let mut store = EntityMetaStore::new();
219        let old = store.alloc();
220        store.dealloc(old);
221        let _new = store.alloc();
222        assert!(!store.is_alive(old));
223    }
224
225    #[test]
226    fn meta_store_double_dealloc_returns_false() {
227        let mut store = EntityMetaStore::new();
228        let e = store.alloc();
229        assert!(store.dealloc(e));
230        assert!(!store.dealloc(e));
231    }
232
233    #[test]
234    fn meta_store_alive_entities_iterates_only_living() {
235        let mut store = EntityMetaStore::new();
236        let _e0 = store.alloc();
237        let e1 = store.alloc();
238        let _e2 = store.alloc();
239        store.dealloc(e1);
240
241        let alive: Vec<_> = store.alive_entities().collect();
242        assert_eq!(alive.len(), 2);
243        assert_eq!(alive[0].index, 0);
244        assert_eq!(alive[1].index, 2);
245    }
246
247    #[test]
248    fn meta_store_location_tracking() {
249        let mut store = EntityMetaStore::new();
250        let e = store.alloc();
251
252        // No location initially.
253        assert_eq!(store.get_location(e), None);
254
255        let loc = EntityLocation {
256            archetype_id: ArchetypeId(0),
257            row: 3,
258        };
259        store.set_location(e, loc);
260        assert_eq!(store.get_location(e), Some(loc));
261
262        // Dealloc clears location.
263        store.dealloc(e);
264        assert_eq!(store.get_location(e), None);
265    }
266
267    #[test]
268    fn meta_store_realloc_clears_location() {
269        let mut store = EntityMetaStore::new();
270        let e = store.alloc();
271        store.set_location(
272            e,
273            EntityLocation {
274                archetype_id: ArchetypeId(5),
275                row: 2,
276            },
277        );
278        store.dealloc(e);
279
280        let e2 = store.alloc(); // reuses slot 0
281        assert_eq!(e2.index, 0);
282        assert_eq!(store.get_location(e2), None); // location cleared
283    }
284}