Skip to main content

naia_shared/world/
entity_index.rs

1use std::{collections::VecDeque, time::Duration};
2
3use naia_socket_shared::Instant;
4
5/// Per-user dense index for a `GlobalEntity` known to a `UserDiffHandler`.
6///
7/// Phase 8.1 Stage A introduces this newtype as the in-process key for
8/// dirty-set tracking and (eventually) packed mask storage. Each user's
9/// `UserDiffHandler` issues one `EntityIndex` per `GlobalEntity` it
10/// observes via [`crate::KeyGenerator32`], recycling on
11/// `deregister_component` once the entity has no remaining components in
12/// the user's receiver map. `u32` instead of `u16` because the index space
13/// is per-user (16K indices isn't always enough at multi-thousand entity
14/// scopes) and because `KeyGenerator`'s u16 wrap-around bug was already
15/// noted as a follow-up item.
16///
17/// **Wire-format independent.** This index never crosses the wire; it's
18/// purely an in-memory shortcut so dirty queues and (Stage B) bit-vec
19/// membership tests can use Vec-indexed operations instead of HashMap
20/// probes.
21#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
22pub struct EntityIndex(pub u32);
23
24impl From<u32> for EntityIndex {
25    fn from(value: u32) -> Self {
26        Self(value)
27    }
28}
29
30impl From<EntityIndex> for u32 {
31    fn from(value: EntityIndex) -> Self {
32        value.0
33    }
34}
35
36/// u32 variant of [`crate::KeyGenerator`] — same recycling semantics, wider
37/// key space. Forked rather than generic-ified to keep the existing u16
38/// generator's call sites untouched.
39#[derive(Clone)]
40pub struct KeyGenerator32<K: From<u32> + Into<u32> + Copy> {
41    recycling_keys: VecDeque<(u32, Instant)>,
42    recycled_keys: VecDeque<u32>,
43    recycle_timeout: Duration,
44    next_new_key: u32,
45    phantom: std::marker::PhantomData<K>,
46}
47
48impl<K: From<u32> + Into<u32> + Copy> KeyGenerator32<K> {
49    /// Creates a generator that quarantines recycled keys for at least `recycle_timeout` before reissuing them.
50    pub fn new(recycle_timeout: Duration) -> Self {
51        Self {
52            recycle_timeout,
53            recycling_keys: VecDeque::new(),
54            recycled_keys: VecDeque::new(),
55            next_new_key: 0,
56            phantom: std::marker::PhantomData,
57        }
58    }
59
60    /// Issues the next available key, preferring recycled keys whose quarantine period has elapsed.
61    pub fn generate(&mut self) -> K {
62        let now = Instant::now();
63        loop {
64            let Some((_, instant)) = self.recycling_keys.front() else {
65                break;
66            };
67            if instant.elapsed(&now) < self.recycle_timeout {
68                break;
69            }
70            let (key, _) = self.recycling_keys.pop_front().unwrap();
71            self.recycled_keys.push_back(key);
72        }
73        if let Some(key) = self.recycled_keys.pop_front() {
74            return K::from(key);
75        }
76        let output = self.next_new_key;
77        self.next_new_key = self.next_new_key.wrapping_add(1);
78        K::from(output)
79    }
80
81    /// Marks `key` as available for reuse after the configured quarantine period expires.
82    pub fn recycle_key(&mut self, key: &K) {
83        let key_u32: u32 = (*key).into();
84        self.recycling_keys.push_back((key_u32, Instant::now()));
85    }
86
87    /// Highest index ever issued + 1. Used to size bit-vec storage in Stage B.
88    pub fn capacity_hint(&self) -> u32 {
89        self.next_new_key
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn generates_sequential_keys() {
99        let mut g: KeyGenerator32<EntityIndex> = KeyGenerator32::new(Duration::from_secs(60));
100        assert_eq!(g.generate().0, 0);
101        assert_eq!(g.generate().0, 1);
102        assert_eq!(g.generate().0, 2);
103    }
104
105    #[test]
106    fn capacity_hint_matches_next_new_key() {
107        let mut g: KeyGenerator32<EntityIndex> = KeyGenerator32::new(Duration::from_secs(60));
108        assert_eq!(g.capacity_hint(), 0);
109        let _ = g.generate();
110        let _ = g.generate();
111        assert_eq!(g.capacity_hint(), 2);
112    }
113
114    #[test]
115    fn recycle_keeps_key_quarantined_until_timeout() {
116        let mut g: KeyGenerator32<EntityIndex> = KeyGenerator32::new(Duration::from_secs(60));
117        let k = g.generate();
118        g.recycle_key(&k);
119        let next = g.generate();
120        assert_ne!(next.0, k.0, "recycled key should not return before timeout");
121    }
122
123    #[test]
124    fn recycle_returns_after_timeout() {
125        let mut g: KeyGenerator32<EntityIndex> = KeyGenerator32::new(Duration::from_millis(0));
126        let k = g.generate();
127        g.recycle_key(&k);
128        // Spin briefly to ensure elapsed > 0
129        std::thread::sleep(Duration::from_millis(2));
130        let next = g.generate();
131        assert_eq!(next.0, k.0, "recycled key should be reused after timeout");
132    }
133}