Skip to main content

elura_netcode/
entity_prediction.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::{NetcodeError, NetcodeResult};
6
7/// Client-generated stable key used to match a predicted spawn with its authoritative entity.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
9pub struct PredictionKey(pub u64);
10
11/// Monotonic prediction-key allocator.
12#[derive(Debug, Clone)]
13pub struct PredictionKeyGenerator {
14    next: u64,
15}
16
17impl Default for PredictionKeyGenerator {
18    fn default() -> Self {
19        Self { next: 1 }
20    }
21}
22
23impl PredictionKeyGenerator {
24    /// Creates a generator with an application-restored next key.
25    pub fn with_next(next: u64) -> NetcodeResult<Self> {
26        if next == 0 {
27            return Err(NetcodeError::InvalidConfig(
28                "next prediction key must be positive",
29            ));
30        }
31        Ok(Self { next })
32    }
33
34    /// Allocates the next non-zero prediction key.
35    pub fn generate(&mut self) -> NetcodeResult<PredictionKey> {
36        let key = self.next;
37        self.next = self
38            .next
39            .checked_add(1)
40            .ok_or(NetcodeError::SequenceExhausted)?;
41        Ok(PredictionKey(key))
42    }
43}
44
45/// Bounds for unmatched locally predicted entities.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(default, deny_unknown_fields)]
48#[non_exhaustive]
49pub struct PredictedEntityConfig {
50    /// Maximum pending predicted entities.
51    pub capacity: usize,
52    /// Maximum age before an unmatched prediction expires.
53    pub timeout_ticks: u64,
54}
55
56impl Default for PredictedEntityConfig {
57    fn default() -> Self {
58        Self {
59            capacity: 128,
60            timeout_ticks: 120,
61        }
62    }
63}
64
65impl PredictedEntityConfig {
66    /// Validates matcher capacity and expiry bounds.
67    pub fn validate(&self) -> NetcodeResult<()> {
68        if self.capacity == 0 || self.timeout_ticks == 0 {
69            return Err(NetcodeError::InvalidConfig(
70                "predicted entity limits must be positive",
71            ));
72        }
73        Ok(())
74    }
75}
76
77/// One locally spawned entity waiting for authoritative identity.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct PredictedEntity<I> {
80    /// Client prediction key included in the authoritative spawn request/state.
81    pub key: PredictionKey,
82    /// Temporary application-owned client entity identifier.
83    pub temporary_entity: I,
84    /// Client Tick when the predicted entity was created.
85    pub spawned_tick: u64,
86}
87
88/// Successful mapping from temporary entity identity to authoritative identity.
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct EntityMatch<I> {
91    /// Matched prediction key.
92    pub key: PredictionKey,
93    /// Temporary client entity to replace or merge.
94    pub temporary_entity: I,
95    /// Authoritative server entity identity.
96    pub authoritative_entity: I,
97    /// Ticks elapsed before the match arrived.
98    pub age_ticks: u64,
99}
100
101/// Bounded predicted-spawn registry for one client simulation.
102#[derive(Debug, Clone)]
103pub struct PredictedEntityMatcher<I> {
104    config: PredictedEntityConfig,
105    pending: BTreeMap<PredictionKey, PredictedEntity<I>>,
106    temporary: BTreeMap<I, PredictionKey>,
107}
108
109impl<I> PredictedEntityMatcher<I>
110where
111    I: Ord + Clone,
112{
113    /// Creates an empty predicted-entity matcher.
114    pub fn new(config: PredictedEntityConfig) -> NetcodeResult<Self> {
115        config.validate()?;
116        Ok(Self {
117            config,
118            pending: BTreeMap::new(),
119            temporary: BTreeMap::new(),
120        })
121    }
122
123    /// Registers one temporary entity under a unique prediction key.
124    pub fn register(
125        &mut self,
126        key: PredictionKey,
127        temporary_entity: I,
128        spawned_tick: u64,
129    ) -> NetcodeResult<()> {
130        if key.0 == 0 || spawned_tick == 0 {
131            return Err(NetcodeError::InvalidInput(
132                "prediction key and spawn Tick must be positive",
133            ));
134        }
135        if self.pending.contains_key(&key) || self.temporary.contains_key(&temporary_entity) {
136            return Err(NetcodeError::InvalidInput(
137                "prediction key or temporary entity is already registered",
138            ));
139        }
140        if self.pending.len() >= self.config.capacity {
141            return Err(NetcodeError::PredictedEntityLimit);
142        }
143        self.temporary.insert(temporary_entity.clone(), key);
144        self.pending.insert(
145            key,
146            PredictedEntity {
147                key,
148                temporary_entity,
149                spawned_tick,
150            },
151        );
152        Ok(())
153    }
154
155    /// Resolves one authoritative spawn and removes its temporary mapping.
156    pub fn resolve(
157        &mut self,
158        key: PredictionKey,
159        authoritative_entity: I,
160        current_tick: u64,
161    ) -> NetcodeResult<Option<EntityMatch<I>>> {
162        let Some(predicted) = self.pending.get(&key) else {
163            return Ok(None);
164        };
165        if current_tick < predicted.spawned_tick {
166            return Err(NetcodeError::InvalidInput(
167                "authoritative match Tick precedes predicted spawn",
168            ));
169        }
170        if current_tick - predicted.spawned_tick > self.config.timeout_ticks {
171            let predicted = self.pending.remove(&key).unwrap();
172            self.temporary.remove(&predicted.temporary_entity);
173            return Ok(None);
174        }
175        let predicted = self.pending.remove(&key).unwrap();
176        self.temporary.remove(&predicted.temporary_entity);
177        Ok(Some(EntityMatch {
178            key,
179            temporary_entity: predicted.temporary_entity,
180            authoritative_entity,
181            age_ticks: current_tick - predicted.spawned_tick,
182        }))
183    }
184
185    /// Removes and returns predictions older than the configured timeout.
186    pub fn expire(&mut self, current_tick: u64) -> Vec<PredictedEntity<I>> {
187        let expired_keys = self
188            .pending
189            .iter()
190            .filter(|(_, predicted)| {
191                current_tick.saturating_sub(predicted.spawned_tick) > self.config.timeout_ticks
192            })
193            .map(|(key, _)| *key)
194            .collect::<Vec<_>>();
195        expired_keys
196            .into_iter()
197            .filter_map(|key| {
198                let predicted = self.pending.remove(&key)?;
199                self.temporary.remove(&predicted.temporary_entity);
200                Some(predicted)
201            })
202            .collect()
203    }
204
205    /// Returns the number of unmatched predicted entities.
206    pub fn len(&self) -> usize {
207        self.pending.len()
208    }
209
210    /// Returns whether no predicted entity is waiting for a match.
211    pub fn is_empty(&self) -> bool {
212        self.pending.is_empty()
213    }
214
215    /// Returns a pending prediction by key.
216    pub fn get(&self, key: PredictionKey) -> Option<&PredictedEntity<I>> {
217        self.pending.get(&key)
218    }
219
220    /// Clears all unmatched predicted entities.
221    pub fn clear(&mut self) {
222        self.pending.clear();
223        self.temporary.clear();
224    }
225}