Skip to main content

concinnity_core/ecs/
storage.rs

1// The closed-world component-storage macro. Given a list of `field => type, id`
2// triples it generates the per-type `Column`-backed storage struct, the access
3// trait that resolves a component type to its column (and its component id) at
4// compile time, and the generic storage operations: typed push, entity-targeted
5// insert/remove, whole-entity despawn, drain, mutable access, counts, and
6// read-only multi-component joins.
7//
8// The struct also owns the `JoinIndex` keyed by entity id, kept in sync by every
9// structural edit so a multi-component query can find an entity's row in each
10// column without scanning. The single hazard the maintenance must respect: a
11// swap-remove moves the column's last row into the freed slot, so the moved
12// row's owner needs its recorded row patched, or a later join probe reads the
13// wrong row.
14//
15// The registry pairs this with its own asset/blob codegen: `define_components!`
16// calls this for the storage half and adds the asset-enum dispatch
17// (`push(ComponentAsset)`, `all_defs`) in a separate impl block. The storage
18// layout, access trait, and join live here so the component query shares one
19// definition, and so the registered component set stays the only thing that
20// names concrete component types.
21
22/// Generate a component storage for a fixed set of component types: one
23/// `Column<T>` per type, the entity allocator, the shared change tick, the join
24/// index, and the `slot` access trait that resolves a type to its column at
25/// compile time.
26///
27/// The expanding site names the concrete component types; this module stays
28/// engine-agnostic.
29#[macro_export]
30macro_rules! define_component_storage {
31    (
32        storage: $storage:ident,
33        slot: $slot:ident,
34        $( $field:ident => $ty:path, $disc:expr ),+ $(,)?
35    ) => {
36        /// One `Column<T>` per registered component type, the entity allocator
37        /// that stamps each row's id, the change tick stamped on every structural
38        /// edit, and the join index that maps an entity to its row in each
39        /// column. Field columns are the caller's field idents, reached through
40        /// the `$slot` trait; callers never name them directly.
41        #[expect(non_snake_case, reason = "field columns take the caller's idents")]
42        #[derive(Default, Debug)]
43        pub struct $storage {
44            $(
45                /// The column holding every row of one registered component type.
46                pub $field: $crate::ecs::Column<$ty>,
47            )+
48            entities: $crate::ecs::Entities,
49            change_tick: $crate::ecs::AtomicTick,
50            join: $crate::ecs::JoinIndex,
51        }
52
53        impl $storage {
54            /// Push a statically-typed component into its column, minting a fresh
55            /// Entity for the new row and recording it in the join index.
56            pub fn push_typed<C: $slot>(&mut self, c: C) -> $crate::ecs::Entity {
57                let entity = self.entities.alloc();
58                let tick = self.change_tick.bump();
59                let col = C::slot_mut(self);
60                col.push(entity, c, tick);
61                let row = (col.len() - 1) as u32;
62                self.join.set(entity, $crate::ecs::ComponentId::new(C::DISCRIMINANT), row);
63                entity
64            }
65
66            /// Pre-size a component's column ahead of a bulk load (e.g. from a
67            /// blob manifest's per-type counts). Unknown ids are ignored.
68            pub fn reserve(&mut self, component: $crate::ecs::ComponentId, additional: usize) {
69                $(
70                    if component == $crate::ecs::ComponentId::new($disc) {
71                        self.$field.reserve(additional);
72                        return;
73                    }
74                )+
75            }
76
77            /// Allocate a bare entity that owns no components yet. Useful for
78            /// gameplay-only entities and as the target of later `insert_typed`.
79            pub fn spawn(&mut self) -> $crate::ecs::Entity {
80                self.entities.alloc()
81            }
82
83            /// Whether a handle refers to a currently-live entity.
84            pub fn is_alive(&self, entity: $crate::ecs::Entity) -> bool {
85                self.entities.is_alive(entity)
86            }
87
88            /// Add component C to an existing entity. Unlike `push_typed` this does
89            /// not mint an entity: it is how an entity comes to own more than one
90            /// component. The entity must be alive and must not already have C
91            /// (a second row for the same (entity, C) would desync the join).
92            pub fn insert_typed<C: $slot>(&mut self, entity: $crate::ecs::Entity, c: C) {
93                let id = $crate::ecs::ComponentId::new(C::DISCRIMINANT);
94                debug_assert!(
95                    self.entities.is_alive(entity),
96                    "insert_typed on a despawned entity",
97                );
98                debug_assert!(
99                    self.join.row(entity, id).is_none(),
100                    "insert_typed: entity already has this component",
101                );
102                let tick = self.change_tick.bump();
103                let col = C::slot_mut(self);
104                col.push(entity, c, tick);
105                let row = (col.len() - 1) as u32;
106                self.join.set(entity, id, row);
107            }
108
109            /// Remove component C from an entity (leaving the entity alive and any
110            /// other components intact), returning the value if present. Swap-
111            /// remove moves the column's last row into the freed slot, so the
112            /// moved row's owner has its recorded row patched.
113            pub fn remove_typed<C: $slot>(&mut self, entity: $crate::ecs::Entity) -> Option<C> {
114                let id = $crate::ecs::ComponentId::new(C::DISCRIMINANT);
115                let row = self.join.row(entity, id)? as usize;
116                let tick = self.change_tick.bump();
117                let col = C::slot_mut(self);
118                let last = col.len() - 1;
119                let moved = if row != last { Some(col.entities()[last]) } else { None };
120                let value = col.swap_remove(row, tick);
121                self.join.clear(entity, id);
122                if let Some(moved) = moved {
123                    self.join.set(moved, id, row as u32);
124                }
125                Some(value)
126            }
127
128            /// Despawn an entity: swap-remove its row from every column it has,
129            /// patching each moved tail row, then recycle the entity id. This is
130            /// the structural-change primitive runtime despawn is built on.
131            pub fn despawn(&mut self, entity: $crate::ecs::Entity) {
132                if !self.entities.is_alive(entity) {
133                    return;
134                }
135                let tick = self.change_tick.bump();
136                $(
137                    {
138                        let id = $crate::ecs::ComponentId::new(<$ty as $slot>::DISCRIMINANT);
139                        if let Some(row) = self.join.row(entity, id) {
140                            let row = row as usize;
141                            let col = &mut self.$field;
142                            let last = col.len() - 1;
143                            let moved =
144                                if row != last { Some(col.entities()[last]) } else { None };
145                            col.swap_remove(row, tick);
146                            if let Some(moved) = moved {
147                                self.join.set(moved, id, row as u32);
148                            }
149                        }
150                    }
151                )+
152                self.join.clear_entity(entity);
153                self.entities.despawn(entity);
154            }
155
156            /// Remove and return every component of type C. Each owner loses
157            /// only its C component; an owner that has no other component left is
158            /// despawned so its Entity recycles. An owner that still has other
159            /// components stays alive with those intact and join-reachable. The
160            /// whole C column empties at once, so no per-row tail patch is needed
161            /// for C; only each owner's C entry in the join is cleared.
162            pub fn drain<C: $slot>(&mut self) -> ::alloc::vec::Vec<C> {
163                let id = $crate::ecs::ComponentId::new(C::DISCRIMINANT);
164                let owners = C::slot(self).entities().to_vec();
165                let tick = self.change_tick.bump();
166                let drained = C::slot_mut(self).drain(tick);
167                for entity in owners {
168                    self.join.clear(entity, id);
169                    if self.join.mask(entity).is_empty() {
170                        self.entities.despawn(entity);
171                    }
172                }
173                drained
174            }
175
176            /// Mutable slice of every component of type C, stamping the change
177            /// tick because any element may be written.
178            pub fn values_mut<C: $slot>(&mut self) -> &mut [C] {
179                let tick = self.change_tick.bump();
180                C::slot_mut(self).values_mut(tick)
181            }
182
183            /// Mutable iteration over every component of type C paired with its
184            /// owning entity, stamping the change tick because any element may be
185            /// written. The mutable counterpart of the read-only column scan.
186            pub fn values_mut_with_entities<C: $slot>(
187                &mut self,
188            ) -> impl Iterator<Item = ($crate::ecs::Entity, &mut C)> {
189                let tick = self.change_tick.bump();
190                C::slot_mut(self).iter_mut_with_entities(tick)
191            }
192
193            /// The change tick of C's column: the tick at which any C was last
194            /// inserted, removed, or mutably accessed. Read-only, so it never
195            /// bumps the tick itself.
196            pub fn changed_tick<C: $slot>(&self) -> $crate::ecs::Tick {
197                C::slot(self).changed_tick()
198            }
199
200            /// Every tick stamp of C's column at once. A consumer that tracks
201            /// rows individually needs `bulk` and `structural` alongside
202            /// `changed` to know whether the per-row stamps still describe the
203            /// whole change.
204            pub fn column_ticks<C: $slot>(&self) -> $crate::ecs::ColumnTicks {
205                C::slot(self).ticks()
206            }
207
208            /// Rows of C written since `since`, paired with their owning entity.
209            /// Only the rows a targeted `get_mut` touched are reported, so this
210            /// is the dirty set a per-frame pass re-examines instead of the whole
211            /// column. Meaningful only while C's `bulk` and `structural` ticks
212            /// have not moved since `since`; past either, every row must be
213            /// treated as changed.
214            pub fn changed_rows<C: $slot>(
215                &self,
216                since: $crate::ecs::Tick,
217            ) -> impl Iterator<Item = ($crate::ecs::Entity, &C)> {
218                C::slot(self).changed_rows(since.clamp_to(self.change_tick.get()))
219            }
220
221            /// Borrow one entity's component C, if it has one.
222            pub fn get<C: $slot>(&self, entity: $crate::ecs::Entity) -> Option<&C> {
223                let row = self.join.row(entity, $crate::ecs::ComponentId::new(C::DISCRIMINANT))?;
224                C::slot(self).get(row as usize)
225            }
226
227            /// Mutably borrow one entity's component C, stamping that row's
228            /// change tick (and the column's) but not the bulk tick, so
229            /// `changed_rows` can report exactly this entity.
230            pub fn get_mut<C: $slot>(&mut self, entity: $crate::ecs::Entity) -> Option<&mut C> {
231                let id = $crate::ecs::ComponentId::new(C::DISCRIMINANT);
232                let row = self.join.row(entity, id)? as usize;
233                let tick = self.change_tick.bump();
234                C::slot_mut(self).value_mut(row, tick)
235            }
236
237            /// Read-only join over two component types. Iterates the first type's
238            /// rows and, for each owning entity that also has the second type,
239            /// yields both component refs. This is the multi-component query for
240            /// read paths (the draw-list push, scene visibility): one column scan
241            /// plus a join probe per row, no allocation.
242            pub fn join2<'s, A: $slot, B: $slot>(
243                &'s self,
244            ) -> impl Iterator<Item = ($crate::ecs::Entity, &'s A, &'s B)> + 's {
245                let bid = $crate::ecs::ComponentId::new(B::DISCRIMINANT);
246                let bcol = B::slot(self);
247                A::slot(self)
248                    .iter_with_entities()
249                    .filter_map(move |(entity, a)| {
250                        let brow = self.join.row(entity, bid)? as usize;
251                        let b = bcol.get(brow);
252                        debug_assert!(
253                            b.is_some(),
254                            "join2: stale JoinIndex row for an entity's component",
255                        );
256                        Some((entity, a, b?))
257                    })
258            }
259
260            /// Read-only join over three component types, lead on the first.
261            pub fn join3<'s, A: $slot, B: $slot, C: $slot>(
262                &'s self,
263            ) -> impl Iterator<Item = ($crate::ecs::Entity, &'s A, &'s B, &'s C)> + 's {
264                let bid = $crate::ecs::ComponentId::new(B::DISCRIMINANT);
265                let cid = $crate::ecs::ComponentId::new(C::DISCRIMINANT);
266                let bcol = B::slot(self);
267                let ccol = C::slot(self);
268                A::slot(self)
269                    .iter_with_entities()
270                    .filter_map(move |(entity, a)| {
271                        let brow = self.join.row(entity, bid)? as usize;
272                        let crow = self.join.row(entity, cid)? as usize;
273                        let b = bcol.get(brow);
274                        let c = ccol.get(crow);
275                        debug_assert!(
276                            b.is_some() && c.is_some(),
277                            "join3: stale JoinIndex row for an entity's component",
278                        );
279                        Some((entity, a, b?, c?))
280                    })
281            }
282
283            /// Total number of components across all typed columns.
284            pub fn len(&self) -> usize {
285                0 $( + self.$field.len() )+
286            }
287
288            /// Whether every typed column is empty.
289            pub fn is_empty(&self) -> bool {
290                true $( && self.$field.is_empty() )+
291            }
292        }
293
294        /// Resolves a component type to its column inside the storage at compile
295        /// time, so the generic storage operations above need no runtime
296        /// dispatch. A registered component is exactly a type with a `$slot` impl,
297        /// and `DISCRIMINANT` is its stable id, used as its `ComponentId` in the
298        /// join index. `'static`: components own their data, and the generic ops
299        /// hand out borrows of (and owned vectors of) the type.
300        pub trait $slot: Sized + 'static {
301            /// The component type's stable id, used as its `ComponentId`.
302            const DISCRIMINANT: u8;
303            /// Borrow this type's column out of the storage.
304            fn slot(s: &$storage) -> &$crate::ecs::Column<Self>;
305            /// Mutably borrow this type's column out of the storage.
306            fn slot_mut(s: &mut $storage) -> &mut $crate::ecs::Column<Self>;
307        }
308
309        $(
310            impl $slot for $ty {
311                const DISCRIMINANT: u8 = $disc;
312                fn slot(s: &$storage) -> &$crate::ecs::Column<Self> { &s.$field }
313                fn slot_mut(s: &mut $storage) -> &mut $crate::ecs::Column<Self> { &mut s.$field }
314            }
315            // The ComponentMask is a u128, so a discriminant past 127 would
316            // silently alias another component's mask bit in a release build.
317            // Make that a build error at the registration site instead.
318            const _: () = assert!(
319                $disc <= $crate::ecs::ComponentId::MAX,
320                "component discriminant exceeds the 127-bit ComponentMask ceiling",
321            );
322        )+
323    };
324}
325
326#[cfg(test)]
327mod tests {
328    // TestStorage is private to this module, so dead_code fires on whichever
329    // generated methods these tests happen not to call. Scoped here rather than
330    // emitted by the macro, which would put a suppression in every consumer's
331    // expansion. (The engine's own `ComponentStorage` is public API, so the
332    // lint never reaches it either way.)
333    #![expect(
334        dead_code,
335        unreachable_pub,
336        reason = "TestStorage is module-private, so the generated pub items are unreachable and dead_code fires on whichever ones these tests skip"
337    )]
338
339    use std::vec;
340    use std::vec::Vec;
341    // `pub` so the generated `pub` columns don't expose a more-private type
342    // (the real engine's component types are `pub`, so this never bites there).
343    #[derive(Default, Debug, PartialEq, Clone, Copy)]
344    #[expect(
345        unreachable_pub,
346        reason = "pub so the generated pub columns do not expose a more-private type"
347    )]
348    pub struct Position(u32);
349
350    #[derive(Default, Debug, PartialEq, Clone, Copy)]
351    #[expect(
352        unreachable_pub,
353        reason = "pub so the generated pub columns do not expose a more-private type"
354    )]
355    pub struct Velocity(i32);
356
357    #[derive(Default, Debug, PartialEq, Clone, Copy)]
358    #[expect(
359        unreachable_pub,
360        reason = "pub so the generated pub columns do not expose a more-private type"
361    )]
362    pub struct Tag;
363
364    define_component_storage! {
365        storage: TestStorage,
366        slot: TestSlot,
367        Position => Position, 1,
368        Velocity => Velocity, 2,
369        Tag => Tag, 3,
370    }
371
372    // `reserve` pre-sizes exactly the addressed column; unknown ids are a
373    // no-op, and reserved capacity survives subsequent pushes.
374    #[test]
375    fn reserve_presizes_the_addressed_column() {
376        let mut s = TestStorage::default();
377        s.reserve(crate::ecs::ComponentId::new(1), 64);
378        assert!(s.Position.capacity() >= 64);
379        assert_eq!(s.Velocity.capacity(), 0, "other columns untouched");
380        s.reserve(crate::ecs::ComponentId::new(99), 64); // unregistered id: ignored
381        s.push_typed(Position(1));
382        assert!(s.Position.capacity() >= 64);
383        assert_eq!(s.len(), 1);
384    }
385
386    #[test]
387    fn push_count_mutate_drain() {
388        let mut s = TestStorage::default();
389        assert!(s.is_empty());
390        assert_eq!(s.len(), 0);
391
392        s.push_typed(Position(1));
393        s.push_typed(Position(2));
394        s.push_typed(Velocity(-3));
395        assert!(!s.is_empty());
396        assert_eq!(s.len(), 3);
397
398        // values_mut resolves the type to its own column.
399        for p in s.values_mut::<Position>() {
400            p.0 += 10;
401        }
402
403        // Draining one type leaves the other untouched.
404        assert_eq!(s.drain::<Position>(), vec![Position(11), Position(12)]);
405        assert_eq!(s.len(), 1);
406        assert_eq!(s.drain::<Velocity>(), vec![Velocity(-3)]);
407        assert!(s.is_empty());
408    }
409
410    #[test]
411    fn columns_carry_row_aligned_entities() {
412        let mut s = TestStorage::default();
413        let a = s.push_typed(Position(7));
414        let b = s.push_typed(Position(8));
415        // Each pushed row got a distinct Entity, aligned with the data.
416        let entities = <Position as TestSlot>::slot(&s).entities();
417        assert_eq!(entities, &[a, b]);
418        assert_ne!(a, b);
419    }
420
421    #[test]
422    fn insert_puts_two_components_on_one_entity() {
423        let mut s = TestStorage::default();
424        // push_typed mints the entity and gives it its first component.
425        let e = s.push_typed(Position(5));
426        // insert_typed adds a second component to the SAME entity -- the thing
427        // that was impossible while every row minted its own entity.
428        s.insert_typed(e, Velocity(-2));
429        s.insert_typed(e, Tag);
430
431        let joined: Vec<_> = s.join2::<Position, Velocity>().collect();
432        assert_eq!(joined.len(), 1);
433        assert_eq!(joined[0], (e, &Position(5), &Velocity(-2)));
434
435        let joined3: Vec<_> = s.join3::<Position, Velocity, Tag>().collect();
436        assert_eq!(joined3.len(), 1);
437        assert_eq!(joined3[0], (e, &Position(5), &Velocity(-2), &Tag));
438    }
439
440    #[test]
441    fn join2_only_matches_entities_with_both() {
442        let mut s = TestStorage::default();
443        let a = s.push_typed(Position(1));
444        s.insert_typed(a, Velocity(10));
445        // b has only a Position, so it must not appear in the join.
446        let _b = s.push_typed(Position(2));
447        let c = s.push_typed(Position(3));
448        s.insert_typed(c, Velocity(30));
449
450        let mut joined: Vec<_> = s
451            .join2::<Position, Velocity>()
452            .map(|(e, p, v)| (e, *p, *v))
453            .collect();
454        joined.sort_by_key(|(e, _, _)| e.index());
455        assert_eq!(
456            joined,
457            vec![
458                (a, Position(1), Velocity(10)),
459                (c, Position(3), Velocity(30))
460            ]
461        );
462    }
463
464    #[test]
465    fn remove_typed_patches_the_moved_tail_row() {
466        let mut s = TestStorage::default();
467        // Three entities each with a Velocity; removing the middle one swap-moves
468        // the last row into its slot. The join must still find the moved entity.
469        let a = s.push_typed(Velocity(1));
470        let b = s.push_typed(Velocity(2));
471        let c = s.push_typed(Velocity(3));
472
473        let removed = s.remove_typed::<Velocity>(b);
474        assert_eq!(removed, Some(Velocity(2)));
475        // a and c are still readable through the join at their (possibly moved)
476        // rows; b is gone.
477        let joined: std::collections::HashMap<_, _> = s
478            .join2::<Velocity, Velocity>() // self-join echoes the live rows
479            .map(|(e, v, _)| (e, *v))
480            .collect();
481        assert_eq!(joined.get(&a), Some(&Velocity(1)));
482        assert_eq!(joined.get(&c), Some(&Velocity(3)));
483        assert_eq!(joined.get(&b), None);
484        assert_eq!(s.len(), 2);
485    }
486
487    #[test]
488    fn remove_typed_returns_none_when_absent() {
489        let mut s = TestStorage::default();
490        let e = s.push_typed(Position(1));
491        // e has no Velocity.
492        assert_eq!(s.remove_typed::<Velocity>(e), None);
493        // A bare entity has nothing to remove either.
494        let bare = s.spawn();
495        assert_eq!(s.remove_typed::<Position>(bare), None);
496        assert_eq!(s.len(), 1);
497    }
498
499    #[test]
500    fn remove_typed_last_row_takes_the_no_move_branch() {
501        let mut s = TestStorage::default();
502        let a = s.push_typed(Velocity(1));
503        let b = s.push_typed(Velocity(2));
504        let c = s.push_typed(Velocity(3));
505        // Removing the LAST row (c) means row == last, so nothing is swapped in.
506        assert_eq!(s.remove_typed::<Velocity>(c), Some(Velocity(3)));
507        let joined: std::collections::HashMap<_, _> = s
508            .join2::<Velocity, Velocity>()
509            .map(|(e, v, _)| (e, *v))
510            .collect();
511        assert_eq!(joined.get(&a), Some(&Velocity(1)));
512        assert_eq!(joined.get(&b), Some(&Velocity(2)));
513        assert_eq!(joined.get(&c), None);
514        assert_eq!(s.len(), 2);
515    }
516
517    #[test]
518    fn remove_one_component_keeps_siblings_on_a_multi_component_entity() {
519        let mut s = TestStorage::default();
520        // Two multi-component entities; remove a non-tail Velocity row from the
521        // first and confirm both entities' surviving components stay joinable.
522        let a = s.push_typed(Position(1));
523        s.insert_typed(a, Velocity(10));
524        s.insert_typed(a, Tag);
525        let b = s.push_typed(Position(2));
526        s.insert_typed(b, Velocity(20));
527
528        assert_eq!(s.remove_typed::<Velocity>(a), Some(Velocity(10)));
529        assert!(s.is_alive(a));
530        // a kept Position + Tag; b kept Position + Velocity.
531        let pos_tag: Vec<_> = s
532            .join2::<Position, Tag>()
533            .map(|(e, p, _)| (e, *p))
534            .collect();
535        assert_eq!(pos_tag, vec![(a, Position(1))]);
536        let pos_vel: Vec<_> = s
537            .join2::<Position, Velocity>()
538            .map(|(e, p, v)| (e, *p, *v))
539            .collect();
540        assert_eq!(pos_vel, vec![(b, Position(2), Velocity(20))]);
541    }
542
543    #[test]
544    fn remove_then_reinsert_same_component_on_live_entity() {
545        let mut s = TestStorage::default();
546        let a = s.push_typed(Position(1));
547        let b = s.push_typed(Position(2));
548        let _c = s.push_typed(Position(3));
549        s.insert_typed(b, Velocity(20));
550
551        // Remove then re-insert the same component type on the same live entity.
552        // The re-insert must not trip insert_typed's "already has it" assert.
553        assert_eq!(s.remove_typed::<Velocity>(b), Some(Velocity(20)));
554        assert!(s.is_alive(b));
555        s.insert_typed(b, Velocity(21));
556
557        let joined: std::collections::HashMap<_, _> = s
558            .join2::<Position, Velocity>()
559            .map(|(e, p, v)| (e, (*p, *v)))
560            .collect();
561        assert_eq!(joined.get(&b), Some(&(Position(2), Velocity(21))));
562        assert_eq!(joined.get(&a), None);
563    }
564
565    #[test]
566    fn drain_one_type_keeps_shared_entities_and_their_other_components() {
567        let mut s = TestStorage::default();
568        // shared owns Position + Velocity; solo owns only Position.
569        let shared = s.push_typed(Position(1));
570        s.insert_typed(shared, Velocity(99));
571        let solo = s.push_typed(Position(2));
572
573        let drained = s.drain::<Position>();
574        assert_eq!(drained.len(), 2);
575        // solo had only Position, so it is despawned and recycled.
576        assert!(!s.is_alive(solo));
577        // shared still has Velocity, so it stays alive and join-reachable; no
578        // orphaned Velocity row, and len() reflects exactly the one survivor.
579        assert!(s.is_alive(shared));
580        let vels: Vec<_> = s
581            .join2::<Velocity, Velocity>()
582            .map(|(e, v, _)| (e, *v))
583            .collect();
584        assert_eq!(vels, vec![(shared, Velocity(99))]);
585        assert_eq!(s.len(), 1);
586        // Draining the remaining type now despawns shared too.
587        assert_eq!(s.drain::<Velocity>(), vec![Velocity(99)]);
588        assert!(!s.is_alive(shared));
589        assert!(s.is_empty());
590    }
591
592    #[test]
593    fn despawn_removes_all_components_and_patches_tails() {
594        let mut s = TestStorage::default();
595        // e1 has Position+Velocity+Tag; e2 has Position+Velocity. Despawning e1
596        // swap-removes from three columns; e2's rows (the tails) must be patched.
597        let e1 = s.push_typed(Position(1));
598        s.insert_typed(e1, Velocity(11));
599        s.insert_typed(e1, Tag);
600        let e2 = s.push_typed(Position(2));
601        s.insert_typed(e2, Velocity(22));
602
603        s.despawn(e1);
604        assert!(!s.is_alive(e1));
605        assert!(s.is_alive(e2));
606
607        // e2 still joins correctly after the swap-remove reordering.
608        let joined: Vec<_> = s.join2::<Position, Velocity>().collect();
609        assert_eq!(joined, vec![(e2, &Position(2), &Velocity(22))]);
610        // e1 contributed one row to each column; all three are gone.
611        assert_eq!(<Position as TestSlot>::slot(&s).len(), 1);
612        assert_eq!(<Velocity as TestSlot>::slot(&s).len(), 1);
613        assert_eq!(<Tag as TestSlot>::slot(&s).len(), 0);
614    }
615
616    #[test]
617    fn despawn_is_a_noop_on_a_stale_handle() {
618        let mut s = TestStorage::default();
619        let e = s.push_typed(Position(1));
620        s.despawn(e);
621        // Second despawn of the same (now stale) handle does nothing.
622        s.despawn(e);
623        assert_eq!(s.len(), 0);
624    }
625
626    #[test]
627    fn get_and_get_mut_address_one_entity() {
628        let mut s = TestStorage::default();
629        let a = s.push_typed(Position(1));
630        let b = s.push_typed(Position(2));
631        s.insert_typed(a, Velocity(10));
632
633        assert_eq!(s.get::<Position>(a), Some(&Position(1)));
634        assert_eq!(s.get::<Position>(b), Some(&Position(2)));
635        assert_eq!(s.get::<Velocity>(a), Some(&Velocity(10)));
636        // b has no Velocity.
637        assert_eq!(s.get::<Velocity>(b), None);
638
639        if let Some(p) = s.get_mut::<Position>(b) {
640            p.0 = 99;
641        }
642        assert_eq!(s.get::<Position>(b), Some(&Position(99)));
643        // The other entity's row is untouched by the targeted write.
644        assert_eq!(s.get::<Position>(a), Some(&Position(1)));
645    }
646
647    // A targeted `get_mut` stamps one row, so `changed_rows` names exactly the
648    // entity written. The column tick still moves (coarse consumers are
649    // unaffected), but neither the bulk nor the structural stamp does.
650    #[test]
651    fn changed_rows_reports_only_the_row_get_mut_touched() {
652        let mut s = TestStorage::default();
653        let _a = s.push_typed(Position(1));
654        let _b = s.push_typed(Position(2));
655        let c = s.push_typed(Position(3));
656        let before = s.column_ticks::<Position>();
657
658        s.get_mut::<Position>(c).unwrap().0 = 30;
659
660        let seen: Vec<(crate::ecs::Entity, u32)> = s
661            .changed_rows::<Position>(before.changed)
662            .map(|(e, p)| (e, p.0))
663            .collect();
664        assert_eq!(seen, vec![(c, 30)]);
665
666        let after = s.column_ticks::<Position>();
667        assert!(after.changed.is_newer_than(before.changed));
668        assert_eq!(
669            after.bulk, before.bulk,
670            "a targeted write is not a bulk one"
671        );
672        assert_eq!(after.structural, before.structural, "nor a structural one");
673    }
674
675    // A whole-column write leaves the per-row stamps alone, so `changed_rows`
676    // on its own reports nothing: `bulk` is the stamp that says every row
677    // moved, which is why a row-tracking consumer has to consult it.
678    #[test]
679    fn a_bulk_write_moves_only_the_bulk_stamp() {
680        let mut s = TestStorage::default();
681        s.push_typed(Position(1));
682        s.push_typed(Position(2));
683        let before = s.column_ticks::<Position>();
684
685        for p in s.values_mut::<Position>() {
686            p.0 += 10;
687        }
688
689        let after = s.column_ticks::<Position>();
690        assert!(after.bulk.is_newer_than(before.bulk));
691        assert_eq!(after.structural, before.structural);
692        assert_eq!(
693            s.changed_rows::<Position>(before.changed).count(),
694            0,
695            "per-row stamps cannot describe a bulk write",
696        );
697    }
698
699    // Adding or removing a row moves the structural stamp; a targeted write
700    // does not. Past a structural move, row positions and membership have
701    // shifted and the per-row stamps no longer describe the change alone.
702    #[test]
703    fn push_and_remove_move_the_structural_stamp() {
704        let mut s = TestStorage::default();
705        let a = s.push_typed(Position(1));
706        let before = s.column_ticks::<Position>();
707
708        s.get_mut::<Position>(a).unwrap().0 = 5;
709        assert_eq!(s.column_ticks::<Position>().structural, before.structural);
710
711        let b = s.push_typed(Position(2));
712        let grown = s.column_ticks::<Position>();
713        assert!(grown.structural.is_newer_than(before.structural));
714
715        s.remove_typed::<Position>(b);
716        assert!(
717            s.column_ticks::<Position>()
718                .structural
719                .is_newer_than(grown.structural)
720        );
721    }
722
723    // A `since` stale enough that the wrap-relative comparison would alias is
724    // pulled forward instead, so the scan over-reports rather than silently
725    // dropping a row that did change. Over-reporting costs a consumer extra
726    // work; under-reporting would leave it acting on data it believes current.
727    #[test]
728    fn changed_rows_pulls_a_stale_since_forward() {
729        let mut s = TestStorage::default();
730        let a = s.push_typed(Position(1));
731        let b = s.push_typed(Position(2));
732
733        // What a tick that fell more than half the range behind looks like
734        // against the storage's live (still small) tick.
735        let stale = crate::ecs::Tick(2_000_000_000);
736        assert!(
737            !crate::ecs::Tick(1).is_newer_than(stale),
738            "unclamped, the comparison aliases and drops these rows",
739        );
740
741        let seen: Vec<crate::ecs::Entity> =
742            s.changed_rows::<Position>(stale).map(|(e, _)| e).collect();
743        assert_eq!(
744            seen,
745            vec![a, b],
746            "the clamp makes a stale window over-report"
747        );
748    }
749
750    #[test]
751    fn spawn_makes_a_bare_entity_for_later_inserts() {
752        let mut s = TestStorage::default();
753        let e = s.spawn();
754        assert!(s.is_alive(e));
755        assert_eq!(s.len(), 0);
756        s.insert_typed(e, Position(9));
757        s.insert_typed(e, Velocity(-9));
758        let joined: Vec<_> = s.join2::<Position, Velocity>().collect();
759        assert_eq!(joined, vec![(e, &Position(9), &Velocity(-9))]);
760    }
761
762    #[test]
763    fn recycled_entity_index_does_not_report_stale_components() {
764        let mut s = TestStorage::default();
765        let a = s.push_typed(Position(1));
766        s.insert_typed(a, Velocity(1));
767        s.despawn(a);
768        // Reusing the freed index for a fresh entity must not inherit a's
769        // components through the join.
770        let b = s.spawn();
771        assert_eq!(a.index(), b.index());
772        s.insert_typed(b, Position(2));
773        let joined: Vec<_> = s.join2::<Position, Velocity>().collect();
774        assert!(
775            joined.is_empty(),
776            "b has no Velocity; stale join must not match"
777        );
778        let positions: Vec<_> = s
779            .join2::<Position, Position>()
780            .map(|(e, p, _)| (e, *p))
781            .collect();
782        assert_eq!(positions, vec![(b, Position(2))]);
783    }
784}