Skip to main content

pylon_replication/
view.rs

1//! What one subscriber was sent, kept so the server can see what that
2//! subscriber drew at an earlier tick (lag compensation).
3//!
4//! A [`ViewHistory`] records the frames sent to one subscriber the way the
5//! clients' entity interpolators record them (`EntityInterpolator` in
6//! packages/realtime and packages/csharp), and [`ViewHistory::position_at`]
7//! places an entity at a fractional tick with the same rule. The two agree
8//! exactly for an interpolator with its default options: `holdWhenQuiet`
9//! on and no `snapDistance`.
10//!
11//! The rules, as in the clients:
12//!
13//! - A spawn starts a life with one sample at the spawn tick. A despawn ends
14//!   the open life at its tick. An entity is drawn from its spawn tick until
15//!   the tick its life ended.
16//! - An update adds a sample when the position or a component changed. A
17//!   second update in the same tick replaces the tick's sample. When the
18//!   entity moved and its last sample is older than the tick before, a copy
19//!   of the last sample is added at the tick before: it stood still until
20//!   then.
21//! - A full frame lists every entity the subscriber has. Open lives of
22//!   entities it does not list end at its tick.
23//! - At a render tick, the entity is between the last sample at or before
24//!   it and the next sample, linearly, and holds at the last sample after
25//!   it. There is no extrapolation.
26//!
27//! Positions are the quantized positions times the frame precision, in
28//! f64, as the clients compute them.
29
30use std::collections::{BTreeMap, VecDeque};
31
32use crate::EntityId;
33
34#[derive(Debug, Clone, Copy, PartialEq)]
35struct Sample {
36    tick: u64,
37    pos: [f64; 3],
38    /// Counts component changes, so a sample knows whether its components
39    /// differ from the one before.
40    components: u64,
41}
42
43#[derive(Debug, Clone)]
44struct Life {
45    spawn_tick: u64,
46    despawn_tick: Option<u64>,
47    samples: VecDeque<Sample>,
48}
49
50/// One entity in a frame sent to the subscriber.
51#[derive(Debug, Clone, Copy, PartialEq)]
52pub struct Sent {
53    pub id: EntityId,
54    /// The quantized position the subscriber has after the frame.
55    pub q: [i64; 3],
56    /// True when the frame changed the entity's components for the subscriber.
57    pub components_changed: bool,
58}
59
60/// The entities sent to one subscriber over the last ticks, as its
61/// interpolator holds them. See the module docs.
62#[derive(Debug, Clone)]
63pub struct ViewHistory {
64    keep_ticks: u64,
65    lives: BTreeMap<EntityId, Vec<Life>>,
66    last_tick: Option<u64>,
67    /// The tick of the last prune: pruning every few ticks is enough to
68    /// bound what the history holds.
69    pruned_at: u64,
70}
71
72/// How many ticks between prunes.
73const PRUNE_EVERY: u64 = 8;
74
75impl ViewHistory {
76    /// Keep enough to place entities at ticks down to `keep_ticks` before
77    /// the newest frame.
78    pub fn new(keep_ticks: u64) -> Self {
79        Self {
80            keep_ticks,
81            lives: BTreeMap::new(),
82            last_tick: None,
83            pruned_at: 0,
84        }
85    }
86
87    /// How many ticks back [`position_at`](Self::position_at) can see.
88    pub fn keep_ticks(&self) -> u64 {
89        self.keep_ticks
90    }
91
92    /// The newest tick recorded.
93    pub fn last_tick(&self) -> Option<u64> {
94        self.last_tick
95    }
96
97    /// Forget everything (a new connection, or the subscriber's baseline was
98    /// reset).
99    pub fn clear(&mut self) {
100        self.lives.clear();
101        self.last_tick = None;
102        self.pruned_at = 0;
103    }
104
105    /// Record a frame sent for `tick`. `full` is a frame that lists every
106    /// entity the subscriber has, as `spawned`. Positions are quantized; the
107    /// subscriber places them at `q * precision`.
108    pub fn record(
109        &mut self,
110        tick: u64,
111        precision: f32,
112        full: bool,
113        spawned: &[Sent],
114        updated: &[Sent],
115        despawned: &[EntityId],
116    ) {
117        if self.last_tick.is_some_and(|last| tick < last) {
118            // Ticks went back: a restarted or different shard.
119            self.clear();
120        }
121        self.last_tick = Some(tick);
122        let p = precision as f64;
123        let pos = |q: [i64; 3]| [q[0] as f64 * p, q[1] as f64 * p, q[2] as f64 * p];
124
125        if full {
126            let present: std::collections::HashSet<EntityId> =
127                spawned.iter().map(|s| s.id).collect();
128            for (id, lives) in self.lives.iter_mut() {
129                if let Some(open) = lives.last_mut().filter(|l| l.despawn_tick.is_none()) {
130                    if !present.contains(id) {
131                        open.despawn_tick = Some(tick);
132                    }
133                }
134            }
135            for s in spawned {
136                match self.open_life(s.id) {
137                    Some(life) => Self::push(life, tick, pos(s.q), s.components_changed),
138                    None => self.spawn(s.id, tick, pos(s.q)),
139                }
140            }
141        } else {
142            for id in despawned {
143                if let Some(open) = self.open_life(*id) {
144                    open.despawn_tick = Some(tick);
145                }
146            }
147            for s in spawned {
148                self.spawn(s.id, tick, pos(s.q));
149            }
150            if updated.windows(2).all(|w| w[0].id < w[1].id) {
151                // Ascending ids (what the replicator sends): one walk of the
152                // map in place of a lookup per update.
153                let mut lives = self.lives.iter_mut().peekable();
154                for s in updated {
155                    while lives.next_if(|(id, _)| **id < s.id).is_some() {}
156                    if let Some((_, l)) = lives.next_if(|(id, _)| **id == s.id) {
157                        if let Some(life) = l.last_mut().filter(|l| l.despawn_tick.is_none()) {
158                            Self::push(life, tick, pos(s.q), s.components_changed);
159                        }
160                    }
161                }
162            } else {
163                for s in updated {
164                    if let Some(life) = self.open_life(s.id) {
165                        Self::push(life, tick, pos(s.q), s.components_changed);
166                    }
167                }
168            }
169        }
170        if tick >= self.pruned_at + PRUNE_EVERY || tick < self.pruned_at {
171            self.prune(tick);
172            self.pruned_at = tick;
173        }
174    }
175
176    /// Where the subscriber drew `id` at `render_tick` (fractional), or None
177    /// when the subscriber did not draw it then (not spawned yet, despawned,
178    /// or never sent).
179    pub fn position_at(&self, id: EntityId, render_tick: f64) -> Option<[f64; 3]> {
180        let lives = self.lives.get(&id)?;
181        let life = lives
182            .iter()
183            .find(|l| l.despawn_tick.is_none_or(|d| (d as f64) > render_tick))?;
184        if (life.spawn_tick as f64) > render_tick {
185            return None;
186        }
187        Some(place(&life.samples, render_tick))
188    }
189
190    /// Every entity the subscriber drew at `render_tick` within `radius` of
191    /// `center` (3D distance), with its position then, in id order.
192    pub fn entities_near(
193        &self,
194        center: [f64; 3],
195        radius: f64,
196        render_tick: f64,
197    ) -> Vec<(EntityId, [f64; 3])> {
198        let r2 = radius * radius;
199        let mut out = Vec::new();
200        for id in self.lives.keys() {
201            if let Some(p) = self.position_at(*id, render_tick) {
202                let d = [p[0] - center[0], p[1] - center[1], p[2] - center[2]];
203                if d[0] * d[0] + d[1] * d[1] + d[2] * d[2] <= r2 {
204                    out.push((*id, p));
205                }
206            }
207        }
208        out
209    }
210
211    /// Ids the history holds a life for (open or recently ended).
212    pub fn ids(&self) -> impl Iterator<Item = EntityId> + '_ {
213        self.lives.keys().copied()
214    }
215
216    fn open_life(&mut self, id: EntityId) -> Option<&mut Life> {
217        self.lives
218            .get_mut(&id)?
219            .last_mut()
220            .filter(|l| l.despawn_tick.is_none())
221    }
222
223    fn spawn(&mut self, id: EntityId, tick: u64, pos: [f64; 3]) {
224        let lives = self.lives.entry(id).or_default();
225        if let Some(open) = lives.last_mut().filter(|l| l.despawn_tick.is_none()) {
226            open.despawn_tick = Some(tick);
227        }
228        let mut samples = VecDeque::with_capacity(4);
229        samples.push_back(Sample {
230            tick,
231            pos,
232            components: 0,
233        });
234        lives.push(Life {
235            spawn_tick: tick,
236            despawn_tick: None,
237            samples,
238        });
239    }
240
241    /// Add an update to an open life (see the module docs).
242    fn push(life: &mut Life, tick: u64, pos: [f64; 3], components_changed: bool) {
243        let samples = &mut life.samples;
244        let last = *samples.back().expect("a life has a sample");
245        let moved = last.pos != pos;
246        if !moved && !components_changed {
247            return;
248        }
249        let components = if components_changed {
250            last.components + 1
251        } else {
252            last.components
253        };
254        if last.tick == tick {
255            *samples.back_mut().expect("checked") = Sample {
256                tick,
257                pos,
258                components,
259            };
260            return;
261        }
262        if moved && last.tick + 1 < tick {
263            // No update since `last`: it stood still until the tick before.
264            samples.push_back(Sample {
265                tick: tick - 1,
266                ..last
267            });
268        }
269        samples.push_back(Sample {
270            tick,
271            pos,
272            components,
273        });
274    }
275
276    /// Drop what no render tick at or after `tick - keep_ticks` needs: ended
277    /// lives, and samples older than the last one at or before that tick.
278    fn prune(&mut self, tick: u64) {
279        let oldest = tick.saturating_sub(self.keep_ticks);
280        self.lives.retain(|_, lives| {
281            lives.retain(|l| l.despawn_tick.is_none_or(|d| d > oldest));
282            for life in lives.iter_mut() {
283                while life.samples.len() > 1 && life.samples[1].tick <= oldest {
284                    life.samples.pop_front();
285                }
286            }
287            !lives.is_empty()
288        });
289    }
290}
291
292/// The client interpolators' rule: between the last sample at or before
293/// the render tick and the next one, linearly; held at the last sample
294/// after it.
295fn place(samples: &VecDeque<Sample>, render_tick: f64) -> [f64; 3] {
296    let mut i = samples.len() - 1;
297    while i > 0 && (samples[i].tick as f64) > render_tick {
298        i -= 1;
299    }
300    let from = samples[i];
301    let to = samples.get(i + 1).copied().unwrap_or(from);
302    let t = if to.tick == from.tick {
303        0.0
304    } else {
305        ((render_tick - from.tick as f64) / (to.tick as f64 - from.tick as f64)).clamp(0.0, 1.0)
306    };
307    let d = [
308        to.pos[0] - from.pos[0],
309        to.pos[1] - from.pos[1],
310        to.pos[2] - from.pos[2],
311    ];
312    [
313        from.pos[0] + d[0] * t,
314        from.pos[1] + d[1] * t,
315        from.pos[2] + d[2] * t,
316    ]
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322
323    fn s(id: EntityId, q: [i64; 3]) -> Sent {
324        Sent {
325            id,
326            q,
327            components_changed: false,
328        }
329    }
330
331    #[test]
332    fn an_entity_is_placed_between_samples_and_held_after_the_last() {
333        let mut v = ViewHistory::new(20);
334        v.record(1, 1.0, true, &[s(7, [0, 0, 0])], &[], &[]);
335        v.record(2, 1.0, false, &[], &[s(7, [10, 0, 0])], &[]);
336        v.record(3, 1.0, false, &[], &[s(7, [30, 0, 0])], &[]);
337        assert_eq!(v.position_at(7, 1.0), Some([0.0, 0.0, 0.0]));
338        assert_eq!(v.position_at(7, 1.5), Some([5.0, 0.0, 0.0]));
339        assert_eq!(v.position_at(7, 2.25), Some([15.0, 0.0, 0.0]));
340        assert_eq!(v.position_at(7, 9.0), Some([30.0, 0.0, 0.0]));
341        // Before its spawn, the subscriber did not draw it.
342        assert_eq!(v.position_at(7, 0.5), None);
343        assert_eq!(v.position_at(8, 2.0), None);
344    }
345
346    #[test]
347    fn a_quiet_entity_stands_still_until_the_tick_before_it_moves() {
348        let mut v = ViewHistory::new(20);
349        v.record(10, 0.5, false, &[s(1, [0, 0, 0])], &[], &[]);
350        // No update for ticks 11-14: deferred, or it did not move.
351        v.record(15, 0.5, false, &[], &[s(1, [20, 0, 0])], &[]);
352        assert_eq!(v.position_at(1, 13.0), Some([0.0, 0.0, 0.0]));
353        assert_eq!(v.position_at(1, 14.0), Some([0.0, 0.0, 0.0]));
354        assert_eq!(v.position_at(1, 14.5), Some([5.0, 0.0, 0.0]));
355        assert_eq!(v.position_at(1, 15.0), Some([10.0, 0.0, 0.0]));
356    }
357
358    #[test]
359    fn a_despawn_ends_the_life_and_a_respawn_starts_another() {
360        let mut v = ViewHistory::new(20);
361        v.record(1, 1.0, false, &[s(4, [1, 1, 1])], &[], &[]);
362        v.record(3, 1.0, false, &[], &[], &[4]);
363        v.record(5, 1.0, false, &[s(4, [9, 9, 9])], &[], &[]);
364        assert_eq!(v.position_at(4, 2.9), Some([1.0, 1.0, 1.0]));
365        assert_eq!(v.position_at(4, 3.0), None);
366        assert_eq!(v.position_at(4, 4.0), None);
367        assert_eq!(v.position_at(4, 5.0), Some([9.0, 9.0, 9.0]));
368    }
369
370    #[test]
371    fn a_full_frame_ends_lives_it_does_not_list() {
372        let mut v = ViewHistory::new(20);
373        v.record(1, 1.0, true, &[s(1, [0, 0, 0]), s(2, [5, 0, 0])], &[], &[]);
374        v.record(4, 1.0, true, &[s(1, [3, 0, 0])], &[], &[]);
375        assert_eq!(v.position_at(2, 3.5), Some([5.0, 0.0, 0.0]));
376        assert_eq!(v.position_at(2, 4.0), None);
377        // Entity 1 moved: it stood still until tick 3, then moved to 3.
378        assert_eq!(v.position_at(1, 3.0), Some([0.0, 0.0, 0.0]));
379        assert_eq!(v.position_at(1, 4.0), Some([3.0, 0.0, 0.0]));
380    }
381
382    #[test]
383    fn a_component_change_without_a_move_adds_a_sample() {
384        let mut v = ViewHistory::new(20);
385        v.record(1, 1.0, false, &[s(1, [0, 0, 0])], &[], &[]);
386        let hp = Sent {
387            components_changed: true,
388            ..s(1, [0, 0, 0])
389        };
390        v.record(5, 1.0, false, &[], &[hp], &[]);
391        v.record(6, 1.0, false, &[], &[s(1, [10, 0, 0])], &[]);
392        // The tick-5 sample holds it still until 5; it moves from there.
393        assert_eq!(v.position_at(1, 5.0), Some([0.0, 0.0, 0.0]));
394        assert_eq!(v.position_at(1, 5.5), Some([5.0, 0.0, 0.0]));
395    }
396
397    #[test]
398    fn old_samples_and_lives_are_pruned() {
399        let mut v = ViewHistory::new(5);
400        v.record(1, 1.0, false, &[s(1, [0, 0, 0]), s(2, [0, 0, 0])], &[], &[]);
401        v.record(2, 1.0, false, &[], &[], &[2]);
402        for t in 3..=30 {
403            v.record(t, 1.0, false, &[], &[s(1, [t as i64, 0, 0])], &[]);
404        }
405        assert!(!v.lives.contains_key(&2), "an ended life long gone");
406        let samples = &v.lives[&1][0].samples;
407        // At most the window, the prune interval, and a sample either side.
408        assert!(
409            samples.len() <= 5 + PRUNE_EVERY as usize + 2,
410            "{} samples",
411            samples.len()
412        );
413        // What the window covers is still exact.
414        assert_eq!(v.position_at(1, 25.5), Some([25.5, 0.0, 0.0]));
415    }
416
417    #[test]
418    fn updates_in_any_order_land_on_their_entities() {
419        let mut v = ViewHistory::new(20);
420        v.record(
421            1,
422            1.0,
423            false,
424            &[s(1, [0, 0, 0]), s(2, [0, 0, 0]), s(3, [0, 0, 0])],
425            &[],
426            &[],
427        );
428        v.record(2, 1.0, false, &[], &[s(3, [3, 0, 0]), s(1, [1, 0, 0])], &[]);
429        v.record(3, 1.0, false, &[], &[s(1, [2, 0, 0]), s(3, [6, 0, 0])], &[]);
430        assert_eq!(v.position_at(1, 3.0), Some([2.0, 0.0, 0.0]));
431        assert_eq!(v.position_at(2, 3.0), Some([0.0, 0.0, 0.0]));
432        assert_eq!(v.position_at(3, 2.0), Some([3.0, 0.0, 0.0]));
433        assert_eq!(v.position_at(3, 3.0), Some([6.0, 0.0, 0.0]));
434    }
435
436    #[test]
437    fn near_finds_the_entities_within_the_radius_at_the_tick() {
438        let mut v = ViewHistory::new(20);
439        v.record(
440            1,
441            1.0,
442            false,
443            &[s(1, [0, 0, 0]), s(2, [100, 0, 0])],
444            &[],
445            &[],
446        );
447        v.record(2, 1.0, false, &[], &[s(2, [10, 0, 0])], &[]);
448        assert_eq!(
449            v.entities_near([0.0, 0.0, 0.0], 20.0, 1.0),
450            vec![(1, [0.0, 0.0, 0.0])]
451        );
452        assert_eq!(
453            v.entities_near([0.0, 0.0, 0.0], 20.0, 2.0),
454            vec![(1, [0.0, 0.0, 0.0]), (2, [10.0, 0.0, 0.0])]
455        );
456    }
457}