dreamwell-runtime 1.0.0

Dreamwell Runtime — cross-platform GPU-accelerated game client
Documentation
//! Client mirror — read-optimized snapshot of authority state.
//! Updated at tick boundaries from staged authority events.
//! Never written to by render code.

/// Maximum entities the client mirror will track. Prevents OOM from
/// malicious or buggy authority flooding entity spawns.
pub const MAX_MIRROR_ENTITIES: usize = 65536;

/// Client-side mirror of authoritative game state.
/// This is the read model — render code reads from here,
/// authority events write to here at tick boundaries.
#[derive(Debug, Default)]
pub struct ClientMirror {
    /// Entity positions (indexed by entity slot).
    pub positions: Vec<[f32; 3]>,
    /// Entity rotations as quaternions.
    pub rotations: Vec<[f32; 4]>,
    /// Active entity count.
    pub entity_count: usize,
    /// Current tick number from authority.
    pub tick: u64,
    /// Pending despawn slots — processed last in reconciliation to avoid
    /// swap_remove index invalidation during the same tick's events.
    pub(crate) pending_despawns: Vec<usize>,
}

impl ClientMirror {
    pub fn new() -> Self {
        Self::default()
    }

    /// Apply a snapshot chunk from the authority. Respects MAX_MIRROR_ENTITIES cap.
    pub fn apply_snapshot(&mut self, positions: Vec<[f32; 3]>, rotations: Vec<[f32; 4]>) {
        let cap = positions.len().min(MAX_MIRROR_ENTITIES);
        self.entity_count = cap;
        self.positions = positions;
        self.positions.truncate(cap);
        self.rotations = rotations;
        self.rotations.truncate(cap);
    }

    /// Advance tick counter.
    pub fn advance_tick(&mut self) {
        self.tick += 1;
    }

    /// Queue a slot for deferred despawn (applied after all other events).
    pub(crate) fn queue_despawn(&mut self, slot: usize) {
        self.pending_despawns.push(slot);
    }

    /// Apply all queued despawns in reverse order (highest index first)
    /// to avoid swap_remove index invalidation.
    pub(crate) fn flush_despawns(&mut self) {
        self.pending_despawns.sort_unstable();
        self.pending_despawns.dedup();
        // Process highest indices first so swap_remove doesn't invalidate lower indices.
        for &slot in self.pending_despawns.iter().rev() {
            if slot < self.positions.len() {
                self.positions.swap_remove(slot);
                self.rotations.swap_remove(slot);
            }
        }
        self.entity_count = self.positions.len();
        self.pending_despawns.clear();
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn default_mirror() {
        let m = ClientMirror::new();
        assert_eq!(m.entity_count, 0);
        assert_eq!(m.tick, 0);
    }

    #[test]
    fn apply_snapshot() {
        let mut m = ClientMirror::new();
        m.apply_snapshot(vec![[1.0, 2.0, 3.0]], vec![[0.0, 0.0, 0.0, 1.0]]);
        assert_eq!(m.entity_count, 1);
    }

    #[test]
    fn snapshot_respects_cap() {
        let mut m = ClientMirror::new();
        let big: Vec<[f32; 3]> = (0..MAX_MIRROR_ENTITIES + 100).map(|i| [i as f32, 0.0, 0.0]).collect();
        let rots: Vec<[f32; 4]> = vec![[0.0, 0.0, 0.0, 1.0]; big.len()];
        m.apply_snapshot(big, rots);
        assert_eq!(m.entity_count, MAX_MIRROR_ENTITIES);
        assert_eq!(m.positions.len(), MAX_MIRROR_ENTITIES);
    }

    #[test]
    fn deferred_despawn_reverse_order() {
        let mut m = ClientMirror::new();
        m.positions = vec![[0.0; 3]; 5];
        m.rotations = vec![[0.0, 0.0, 0.0, 1.0]; 5];
        m.entity_count = 5;
        m.queue_despawn(1);
        m.queue_despawn(3);
        m.flush_despawns();
        assert_eq!(m.entity_count, 3);
    }
}