Skip to main content

naia_shared/connection/
priority_state.rs

1use std::{collections::HashMap, hash::Hash};
2
3use crate::connection::entity_priority::{EntityPriorityData, EntityPriorityMut, EntityPriorityRef};
4use crate::world::entity::global_entity::GlobalEntity;
5
6/// Per-tick hook the send loop calls to (a) advance accumulators for dirty
7/// entity bundles, and (b) reset accumulators for bundles that fully drained.
8///
9/// The hook is keyed by `GlobalEntity` so it can be implemented inside
10/// `naia-shared` without leaking the server's world-entity type. Implementations
11/// translate to their internal entity representation as needed.
12///
13/// See PRIORITY_ACCUMULATOR_PLAN.md III.7 — the canonical accumulator rules.
14pub trait OutgoingPriorityHook {
15    /// Advance the per-user accumulator for `entity` by its effective gain
16    /// (`global.gain × user.gain`, defaults 1.0) and return the new accumulated
17    /// value. The send loop sorts dirty entity bundles by this value descending
18    /// to drive the k-way merge against the bandwidth budget.
19    fn advance(&mut self, entity: &GlobalEntity) -> f32;
20
21    /// Reset the per-user accumulator for `entity` after its update bundle has
22    /// been fully drained into the wire this tick. Stamps `last_sent_tick`.
23    fn reset_after_send(&mut self, entity: &GlobalEntity, current_tick: u32);
24}
25
26/// Sender-wide priority layer. One instance lives on `WorldServer`.
27/// Entries are evicted on entity despawn.
28///
29/// Combined multiplicatively with each `UserPriorityState` entry at sort time:
30/// `effective_gain = global.gain.unwrap_or(1.0) × user.gain.unwrap_or(1.0)`.
31pub struct GlobalPriorityState<E: Copy + Eq + Hash> {
32    entries: HashMap<E, EntityPriorityData>,
33}
34
35impl<E: Copy + Eq + Hash> GlobalPriorityState<E> {
36    /// Creates an empty `GlobalPriorityState`.
37    pub fn new() -> Self {
38        Self {
39            entries: HashMap::new(),
40        }
41    }
42
43    /// Read-only handle (lazy — returns `None` if no entry exists).
44    pub fn get_ref(&self, entity: E) -> EntityPriorityRef<'_, E> {
45        EntityPriorityRef {
46            state: self.entries.get(&entity),
47            entity,
48        }
49    }
50
51    /// Mutable handle; lazy entry creation deferred to first write.
52    pub fn get_mut(&mut self, entity: E) -> EntityPriorityMut<'_, E> {
53        EntityPriorityMut {
54            entries: &mut self.entries,
55            entity,
56        }
57    }
58
59    /// Evict this entity's global entry. Called from `WorldServer::despawn_entity`.
60    pub fn on_despawn(&mut self, entity: &E) {
61        self.entries.remove(entity);
62    }
63
64    /// Read-only gain lookup. `None` if no override is in effect (default 1.0
65    /// applies). Used by the send-time priority sort to compute effective gain.
66    pub fn gain_override(&self, entity: &E) -> Option<f32> {
67        self.entries.get(entity).and_then(|d| d.gain_override)
68    }
69}
70
71impl<E: Copy + Eq + Hash> Default for GlobalPriorityState<E> {
72    fn default() -> Self {
73        Self::new()
74    }
75}
76
77/// Per-connection priority layer. One instance lives on each user `Connection`
78/// (and on the client's single `Connection`).
79/// Entries are evicted on scope exit for that user.
80pub struct UserPriorityState<E: Copy + Eq + Hash> {
81    entries: HashMap<E, EntityPriorityData>,
82}
83
84impl<E: Copy + Eq + Hash> UserPriorityState<E> {
85    /// Creates an empty `UserPriorityState`.
86    pub fn new() -> Self {
87        Self {
88            entries: HashMap::new(),
89        }
90    }
91
92    /// Returns a read-only priority handle for `entity` in this user's layer.
93    pub fn get_ref(&self, entity: E) -> EntityPriorityRef<'_, E> {
94        EntityPriorityRef {
95            state: self.entries.get(&entity),
96            entity,
97        }
98    }
99
100    /// Returns a mutable priority handle for `entity` in this user's layer.
101    pub fn get_mut(&mut self, entity: E) -> EntityPriorityMut<'_, E> {
102        EntityPriorityMut {
103            entries: &mut self.entries,
104            entity,
105        }
106    }
107
108    /// Evict this entity's per-user entry. Called on scope exit or connection drop.
109    pub fn on_scope_exit(&mut self, entity: &E) {
110        self.entries.remove(entity);
111    }
112
113    /// Read-only gain lookup for this user layer.
114    pub fn gain_override(&self, entity: &E) -> Option<f32> {
115        self.entries.get(entity).and_then(|d| d.gain_override)
116    }
117
118    /// Per-tick advance hook used by the send-side k-way merge.
119    /// Adds `gain` to the entity's accumulator (lazy-creating the entry) and
120    /// returns the new accumulated value.
121    ///
122    /// This is the canonical "accumulator += effective_gain per tick" rule
123    /// from PRIORITY_ACCUMULATOR_PLAN.md III.7.1.
124    pub fn advance(&mut self, entity: E, gain: f32) -> f32 {
125        let entry = self.entries.entry(entity).or_default();
126        entry.accumulated += gain;
127        entry.accumulated
128    }
129
130    /// Read accumulator without advancing.
131    pub fn accumulated(&self, entity: &E) -> f32 {
132        self.entries.get(entity).map(|d| d.accumulated).unwrap_or(0.0)
133    }
134
135    /// Reset the accumulator to 0 and stamp `last_sent_tick`. Called for each
136    /// entity whose update bundle fully drained in the current send cycle —
137    /// the canonical reset-on-send rule from III.7.5.
138    pub fn reset_after_send(&mut self, entity: &E, current_tick: u32) {
139        if let Some(data) = self.entries.get_mut(entity) {
140            data.accumulated = 0.0;
141            data.last_sent_tick = Some(current_tick);
142        }
143    }
144}
145
146impl<E: Copy + Eq + Hash> Default for UserPriorityState<E> {
147    fn default() -> Self {
148        Self::new()
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn global_on_despawn_evicts_entry() {
158        let mut s = GlobalPriorityState::<u32>::new();
159        s.get_mut(7).set_gain(5.0);
160        assert_eq!(s.get_ref(7).gain(), Some(5.0));
161        s.on_despawn(&7);
162        assert_eq!(s.get_ref(7).gain(), None);
163    }
164
165    #[test]
166    fn user_on_scope_exit_evicts_entry() {
167        let mut s = UserPriorityState::<u32>::new();
168        s.get_mut(7).boost_once(3.0);
169        assert_eq!(s.get_ref(7).accumulated(), 3.0);
170        s.on_scope_exit(&7);
171        assert_eq!(s.get_ref(7).accumulated(), 0.0);
172    }
173
174    #[test]
175    fn global_eviction_preserves_other_entries() {
176        let mut s = GlobalPriorityState::<u32>::new();
177        s.get_mut(7).set_gain(5.0);
178        s.get_mut(9).set_gain(2.0);
179        s.on_despawn(&7);
180        assert_eq!(s.get_ref(7).gain(), None);
181        assert_eq!(s.get_ref(9).gain(), Some(2.0));
182    }
183}