Skip to main content

naia_shared/connection/
entity_priority.rs

1use std::{collections::HashMap, hash::Hash};
2
3/// Stored per-entity-bundle accumulator state. One of these per (entity, layer),
4/// where layer is either the sender-wide "global" layer or a per-connection
5/// "per-user" layer.
6#[derive(Clone, Debug, Default)]
7pub(crate) struct EntityPriorityData {
8    /// Running priority accumulator; reset to 0 on send.
9    pub(crate) accumulated: f32,
10    /// User-set persistent gain override. `None` means the default (1.0) applies.
11    pub(crate) gain_override: Option<f32>,
12    /// Sender's game tick at last successful send. Telemetry only; not used in
13    /// priority calculation (the accumulator itself encodes staleness).
14    pub(crate) last_sent_tick: Option<u32>,
15}
16
17/// Read-only view of an entity's priority state in one priority layer
18/// (global OR per-user). Acquired via the corresponding `*_priority()` method
19/// on `WorldServer`, `Client`, or their Bevy-adapter equivalents.
20pub struct EntityPriorityRef<'a, E: Copy + Eq + Hash> {
21    pub(crate) state: Option<&'a EntityPriorityData>,
22    pub(crate) entity: E,
23}
24
25impl<'a, E: Copy + Eq + Hash> EntityPriorityRef<'a, E> {
26    /// Construct an empty read-only handle (no backing entry). Reads return
27    /// defaults: `accumulated() == 0.0`, `gain() == None`. Used when the
28    /// caller wants a handle for an entity whose layer doesn't yet exist.
29    pub fn empty(entity: E) -> Self {
30        Self {
31            state: None,
32            entity,
33        }
34    }
35
36    /// Returns the entity this handle refers to.
37    pub fn entity(&self) -> E {
38        self.entity
39    }
40
41    /// Current accumulated priority value for this layer. Higher = more urgent.
42    /// Returns `0.0` if this entity has no accumulator entry yet.
43    pub fn accumulated(&self) -> f32 {
44        self.state.map(|s| s.accumulated).unwrap_or(0.0)
45    }
46
47    /// Current per-tick gain override for this layer. `None` means the default
48    /// (1.0) applies.
49    pub fn gain(&self) -> Option<f32> {
50        self.state.and_then(|s| s.gain_override)
51    }
52
53    /// Returns `true` if a per-tick gain override is currently active.
54    pub fn is_overridden(&self) -> bool {
55        self.gain().is_some()
56    }
57}
58
59/// Mutable handle for reading and setting an entity's priority in one priority
60/// layer. Lazy-creates a state entry on first write so set-and-forget works
61/// even before the entity enters scope for that user.
62///
63/// Returned by `global_entity_priority_mut` / `user_entity_priority_mut` on the
64/// server, `entity_priority_mut` on the client, and their Bevy-adapter
65/// passthroughs.
66pub struct EntityPriorityMut<'a, E: Copy + Eq + Hash> {
67    pub(crate) entries: &'a mut HashMap<E, EntityPriorityData>,
68    pub(crate) entity: E,
69}
70
71impl<'a, E: Copy + Eq + Hash> EntityPriorityMut<'a, E> {
72    // --- Reads (mirror Ref) ---
73
74    /// Returns the entity this handle refers to.
75    pub fn entity(&self) -> E {
76        self.entity
77    }
78
79    /// Current accumulated priority value. Higher = more urgent. Returns `0.0` if no entry yet.
80    pub fn accumulated(&self) -> f32 {
81        self.entries
82            .get(&self.entity)
83            .map(|s| s.accumulated)
84            .unwrap_or(0.0)
85    }
86
87    /// Current per-tick gain override. `None` means the default (1.0) applies.
88    pub fn gain(&self) -> Option<f32> {
89        self.entries
90            .get(&self.entity)
91            .and_then(|s| s.gain_override)
92    }
93
94    /// Returns `true` if a per-tick gain override is currently active.
95    pub fn is_overridden(&self) -> bool {
96        self.gain().is_some()
97    }
98
99    // --- Writes ---
100
101    /// Set a persistent per-tick gain override for this layer. Stays in effect
102    /// until `reset()` or another `set_gain()` call. Lazy-creates the entry.
103    pub fn set_gain(&mut self, gain: f32) -> &mut Self {
104        self.entries
105            .entry(self.entity)
106            .or_default()
107            .gain_override = Some(gain);
108        self
109    }
110
111    /// One-shot additive boost to the accumulator. Does not change gain.
112    /// Multiple calls in one tick sum additively. Lazy-creates the entry.
113    /// Persists across ticks until the entity is sent (then reset to 0).
114    pub fn boost_once(&mut self, amount: f32) -> &mut Self {
115        self.entries
116            .entry(self.entity)
117            .or_default()
118            .accumulated += amount;
119        self
120    }
121
122    /// Clear the gain override — return to default (1.0). Does NOT clear the
123    /// accumulator value itself, and does NOT remove the entry.
124    pub fn reset(&mut self) -> &mut Self {
125        if let Some(data) = self.entries.get_mut(&self.entity) {
126            data.gain_override = None;
127        }
128        self
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    fn fresh() -> HashMap<u32, EntityPriorityData> {
137        HashMap::new()
138    }
139
140    #[test]
141    fn set_gain_lazy_creates_entry() {
142        let mut entries = fresh();
143        let mut m = EntityPriorityMut {
144            entries: &mut entries,
145            entity: 7u32,
146        };
147        assert_eq!(m.gain(), None);
148        m.set_gain(5.0);
149        assert_eq!(m.gain(), Some(5.0));
150        assert!(m.is_overridden());
151    }
152
153    #[test]
154    fn set_gain_then_reset_returns_to_default() {
155        let mut entries = fresh();
156        let mut m = EntityPriorityMut {
157            entries: &mut entries,
158            entity: 7u32,
159        };
160        m.set_gain(5.0);
161        m.reset();
162        assert_eq!(m.gain(), None);
163        assert!(!m.is_overridden());
164        // Entry retained.
165        assert!(entries.contains_key(&7u32));
166    }
167
168    #[test]
169    fn boost_once_is_additive_and_preserves_gain() {
170        let mut entries = fresh();
171        let mut m = EntityPriorityMut {
172            entries: &mut entries,
173            entity: 7u32,
174        };
175        m.set_gain(3.0);
176        m.boost_once(10.0);
177        m.boost_once(5.0);
178        assert_eq!(m.accumulated(), 15.0);
179        assert_eq!(m.gain(), Some(3.0));
180    }
181
182    #[test]
183    fn boost_once_lazy_creates_entry() {
184        let mut entries = fresh();
185        let mut m = EntityPriorityMut {
186            entries: &mut entries,
187            entity: 7u32,
188        };
189        m.boost_once(4.0);
190        assert_eq!(m.accumulated(), 4.0);
191        // No gain override set by boost.
192        assert_eq!(m.gain(), None);
193    }
194
195    #[test]
196    fn reset_on_absent_entry_is_noop() {
197        let mut entries = fresh();
198        let mut m = EntityPriorityMut {
199            entries: &mut entries,
200            entity: 7u32,
201        };
202        m.reset();
203        assert!(!entries.contains_key(&7u32));
204    }
205
206    // B-BDD-4: set_gain(5.0) then reset() → default applied;
207    // is_overridden() == false; entry still exists.
208    #[test]
209    fn b_bdd_4_set_gain_then_reset_yields_default() {
210        let mut entries = fresh();
211        let mut m = EntityPriorityMut {
212            entries: &mut entries,
213            entity: 7u32,
214        };
215        m.set_gain(5.0);
216        assert!(m.is_overridden());
217        m.reset();
218        assert_eq!(m.gain(), None);
219        assert!(!m.is_overridden());
220        assert!(entries.contains_key(&7u32));
221    }
222
223    // B-BDD-5 (write-side): boost_once(100.0) bumps accumulator +100 immediately
224    // without mutating gain. Reset-on-send is a drain-path concern and lives in
225    // the send loop — not testable at this unit level.
226    #[test]
227    fn b_bdd_5_boost_once_does_not_mutate_gain() {
228        let mut entries = fresh();
229        let mut m = EntityPriorityMut {
230            entries: &mut entries,
231            entity: 7u32,
232        };
233        m.set_gain(3.0);
234        m.boost_once(100.0);
235        assert_eq!(m.accumulated(), 100.0);
236        assert_eq!(m.gain(), Some(3.0));
237    }
238
239    // B-BDD-6: set_gain(5.0) persists across subsequent mutations (boost_once,
240    // additional reads). At this layer, "next tick" is simply any state between
241    // mutations — the entry's gain_override field carries forward until reset.
242    #[test]
243    fn b_bdd_6_set_gain_persists_across_mutations() {
244        let mut entries = fresh();
245        {
246            let mut m = EntityPriorityMut {
247                entries: &mut entries,
248                entity: 7u32,
249            };
250            m.set_gain(5.0);
251            m.boost_once(10.0);
252        }
253        // Re-acquire handle later (simulates next tick's access) — gain persists.
254        let m = EntityPriorityMut {
255            entries: &mut entries,
256            entity: 7u32,
257        };
258        assert_eq!(m.gain(), Some(5.0));
259        assert_eq!(m.accumulated(), 10.0);
260    }
261
262    // Ref-side read API mirrors Mut-side read API — important for `*_priority()`
263    // (read-only) returning handles consistent with mutable handles.
264    #[test]
265    fn ref_reads_match_mut_reads() {
266        let mut entries = fresh();
267        {
268            let mut m = EntityPriorityMut {
269                entries: &mut entries,
270                entity: 7u32,
271            };
272            m.set_gain(2.0);
273            m.boost_once(7.0);
274        }
275        let r = EntityPriorityRef {
276            state: entries.get(&7u32),
277            entity: 7u32,
278        };
279        assert_eq!(r.gain(), Some(2.0));
280        assert_eq!(r.accumulated(), 7.0);
281        assert!(r.is_overridden());
282    }
283
284    // Absent-entry read-only handle returns defaults (no entry needed for reads).
285    #[test]
286    fn empty_ref_reads_defaults() {
287        let r = EntityPriorityRef::<u32>::empty(42);
288        assert_eq!(r.entity(), 42);
289        assert_eq!(r.gain(), None);
290        assert_eq!(r.accumulated(), 0.0);
291        assert!(!r.is_overridden());
292    }
293}