Skip to main content

dreamwell_runtime/
mirror.rs

1//! Client mirror — read-optimized snapshot of authority state.
2//! Updated at tick boundaries from staged authority events.
3//! Never written to by render code.
4
5/// Maximum entities the client mirror will track. Prevents OOM from
6/// malicious or buggy authority flooding entity spawns.
7pub const MAX_MIRROR_ENTITIES: usize = 65536;
8
9/// Client-side mirror of authoritative game state.
10/// This is the read model — render code reads from here,
11/// authority events write to here at tick boundaries.
12#[derive(Debug, Default)]
13pub struct ClientMirror {
14    /// Entity positions (indexed by entity slot).
15    pub positions: Vec<[f32; 3]>,
16    /// Entity rotations as quaternions.
17    pub rotations: Vec<[f32; 4]>,
18    /// Active entity count.
19    pub entity_count: usize,
20    /// Current tick number from authority.
21    pub tick: u64,
22    /// Pending despawn slots — processed last in reconciliation to avoid
23    /// swap_remove index invalidation during the same tick's events.
24    pub(crate) pending_despawns: Vec<usize>,
25}
26
27impl ClientMirror {
28    pub fn new() -> Self {
29        Self::default()
30    }
31
32    /// Apply a snapshot chunk from the authority. Respects MAX_MIRROR_ENTITIES cap.
33    pub fn apply_snapshot(&mut self, positions: Vec<[f32; 3]>, rotations: Vec<[f32; 4]>) {
34        let cap = positions.len().min(MAX_MIRROR_ENTITIES);
35        self.entity_count = cap;
36        self.positions = positions;
37        self.positions.truncate(cap);
38        self.rotations = rotations;
39        self.rotations.truncate(cap);
40    }
41
42    /// Advance tick counter.
43    pub fn advance_tick(&mut self) {
44        self.tick += 1;
45    }
46
47    /// Queue a slot for deferred despawn (applied after all other events).
48    pub(crate) fn queue_despawn(&mut self, slot: usize) {
49        self.pending_despawns.push(slot);
50    }
51
52    /// Apply all queued despawns in reverse order (highest index first)
53    /// to avoid swap_remove index invalidation.
54    pub(crate) fn flush_despawns(&mut self) {
55        self.pending_despawns.sort_unstable();
56        self.pending_despawns.dedup();
57        // Process highest indices first so swap_remove doesn't invalidate lower indices.
58        for &slot in self.pending_despawns.iter().rev() {
59            if slot < self.positions.len() {
60                self.positions.swap_remove(slot);
61                self.rotations.swap_remove(slot);
62            }
63        }
64        self.entity_count = self.positions.len();
65        self.pending_despawns.clear();
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn default_mirror() {
75        let m = ClientMirror::new();
76        assert_eq!(m.entity_count, 0);
77        assert_eq!(m.tick, 0);
78    }
79
80    #[test]
81    fn apply_snapshot() {
82        let mut m = ClientMirror::new();
83        m.apply_snapshot(vec![[1.0, 2.0, 3.0]], vec![[0.0, 0.0, 0.0, 1.0]]);
84        assert_eq!(m.entity_count, 1);
85    }
86
87    #[test]
88    fn snapshot_respects_cap() {
89        let mut m = ClientMirror::new();
90        let big: Vec<[f32; 3]> = (0..MAX_MIRROR_ENTITIES + 100).map(|i| [i as f32, 0.0, 0.0]).collect();
91        let rots: Vec<[f32; 4]> = vec![[0.0, 0.0, 0.0, 1.0]; big.len()];
92        m.apply_snapshot(big, rots);
93        assert_eq!(m.entity_count, MAX_MIRROR_ENTITIES);
94        assert_eq!(m.positions.len(), MAX_MIRROR_ENTITIES);
95    }
96
97    #[test]
98    fn deferred_despawn_reverse_order() {
99        let mut m = ClientMirror::new();
100        m.positions = vec![[0.0; 3]; 5];
101        m.rotations = vec![[0.0, 0.0, 0.0, 1.0]; 5];
102        m.entity_count = 5;
103        m.queue_despawn(1);
104        m.queue_despawn(3);
105        m.flush_despawns();
106        assert_eq!(m.entity_count, 3);
107    }
108}