1use 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 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#[derive(Debug, Clone, Copy, PartialEq)]
52pub struct Sent {
53 pub id: EntityId,
54 pub q: [i64; 3],
56 pub components_changed: bool,
58}
59
60#[derive(Debug, Clone)]
63pub struct ViewHistory {
64 keep_ticks: u64,
65 lives: BTreeMap<EntityId, Vec<Life>>,
66 last_tick: Option<u64>,
67 pruned_at: u64,
70}
71
72const PRUNE_EVERY: u64 = 8;
74
75impl ViewHistory {
76 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 pub fn keep_ticks(&self) -> u64 {
89 self.keep_ticks
90 }
91
92 pub fn last_tick(&self) -> Option<u64> {
94 self.last_tick
95 }
96
97 pub fn clear(&mut self) {
100 self.lives.clear();
101 self.last_tick = None;
102 self.pruned_at = 0;
103 }
104
105 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 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.len() >= self.lives.len().div_ceil(8)
153 && updated.windows(2).all(|w| w[0].id < w[1].id)
154 {
155 let mut lives = self.lives.iter_mut().peekable();
157 for s in updated {
158 while lives.next_if(|(id, _)| **id < s.id).is_some() {}
159 if let Some((_, l)) = lives.next_if(|(id, _)| **id == s.id) {
160 if let Some(life) = l.last_mut().filter(|l| l.despawn_tick.is_none()) {
161 Self::push(life, tick, pos(s.q), s.components_changed);
162 }
163 }
164 }
165 } else {
166 for s in updated {
167 if let Some(life) = self.open_life(s.id) {
168 Self::push(life, tick, pos(s.q), s.components_changed);
169 }
170 }
171 }
172 }
173 if tick >= self.pruned_at + PRUNE_EVERY || tick < self.pruned_at {
174 self.prune(tick);
175 self.pruned_at = tick;
176 }
177 }
178
179 pub fn position_at(&self, id: EntityId, render_tick: f64) -> Option<[f64; 3]> {
183 place_entity(self.lives.get(&id)?, render_tick)
184 }
185
186 pub fn entities_near(
189 &self,
190 center: [f64; 3],
191 radius: f64,
192 render_tick: f64,
193 ) -> Vec<(EntityId, [f64; 3])> {
194 let r2 = radius * radius;
195 let mut out = Vec::new();
196 for (id, lives) in &self.lives {
197 if let Some(p) = place_entity(lives, render_tick) {
198 let d = [p[0] - center[0], p[1] - center[1], p[2] - center[2]];
199 if d[0] * d[0] + d[1] * d[1] + d[2] * d[2] <= r2 {
200 out.push((*id, p));
201 }
202 }
203 }
204 out
205 }
206
207 pub fn ids(&self) -> impl Iterator<Item = EntityId> + '_ {
209 self.lives.keys().copied()
210 }
211
212 fn open_life(&mut self, id: EntityId) -> Option<&mut Life> {
213 self.lives
214 .get_mut(&id)?
215 .last_mut()
216 .filter(|l| l.despawn_tick.is_none())
217 }
218
219 fn spawn(&mut self, id: EntityId, tick: u64, pos: [f64; 3]) {
220 let lives = self.lives.entry(id).or_default();
221 if let Some(open) = lives.last_mut().filter(|l| l.despawn_tick.is_none()) {
222 open.despawn_tick = Some(tick);
223 }
224 let mut samples = VecDeque::with_capacity(4);
225 samples.push_back(Sample {
226 tick,
227 pos,
228 components: 0,
229 });
230 lives.push(Life {
231 spawn_tick: tick,
232 despawn_tick: None,
233 samples,
234 });
235 }
236
237 fn push(life: &mut Life, tick: u64, pos: [f64; 3], components_changed: bool) {
239 let samples = &mut life.samples;
240 let last = *samples.back().expect("a life has a sample");
241 let moved = last.pos != pos;
242 if !moved && !components_changed {
243 return;
244 }
245 let components = if components_changed {
246 last.components + 1
247 } else {
248 last.components
249 };
250 if last.tick == tick {
251 *samples.back_mut().expect("checked") = Sample {
252 tick,
253 pos,
254 components,
255 };
256 return;
257 }
258 if moved && last.tick + 1 < tick {
259 samples.push_back(Sample {
261 tick: tick - 1,
262 ..last
263 });
264 }
265 samples.push_back(Sample {
266 tick,
267 pos,
268 components,
269 });
270 }
271
272 fn prune(&mut self, tick: u64) {
275 let oldest = tick.saturating_sub(self.keep_ticks);
276 self.lives.retain(|_, lives| {
277 lives.retain(|l| l.despawn_tick.is_none_or(|d| d > oldest));
278 for life in lives.iter_mut() {
279 while life.samples.len() > 1 && life.samples[1].tick <= oldest {
280 life.samples.pop_front();
281 }
282 }
283 !lives.is_empty()
284 });
285 }
286}
287
288fn place_entity(lives: &[Life], render_tick: f64) -> Option<[f64; 3]> {
289 let life = lives
290 .iter()
291 .find(|l| l.despawn_tick.is_none_or(|d| (d as f64) > render_tick))?;
292 if (life.spawn_tick as f64) > render_tick {
293 return None;
294 }
295 Some(place(&life.samples, render_tick))
296}
297
298fn place(samples: &VecDeque<Sample>, render_tick: f64) -> [f64; 3] {
302 let mut i = samples.len() - 1;
303 while i > 0 && (samples[i].tick as f64) > render_tick {
304 i -= 1;
305 }
306 let from = samples[i];
307 let to = samples.get(i + 1).copied().unwrap_or(from);
308 let t = if to.tick == from.tick {
309 0.0
310 } else {
311 ((render_tick - from.tick as f64) / (to.tick as f64 - from.tick as f64)).clamp(0.0, 1.0)
312 };
313 let d = [
314 to.pos[0] - from.pos[0],
315 to.pos[1] - from.pos[1],
316 to.pos[2] - from.pos[2],
317 ];
318 [
319 from.pos[0] + d[0] * t,
320 from.pos[1] + d[1] * t,
321 from.pos[2] + d[2] * t,
322 ]
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 fn s(id: EntityId, q: [i64; 3]) -> Sent {
330 Sent {
331 id,
332 q,
333 components_changed: false,
334 }
335 }
336
337 #[test]
338 fn an_entity_is_placed_between_samples_and_held_after_the_last() {
339 let mut v = ViewHistory::new(20);
340 v.record(1, 1.0, true, &[s(7, [0, 0, 0])], &[], &[]);
341 v.record(2, 1.0, false, &[], &[s(7, [10, 0, 0])], &[]);
342 v.record(3, 1.0, false, &[], &[s(7, [30, 0, 0])], &[]);
343 assert_eq!(v.position_at(7, 1.0), Some([0.0, 0.0, 0.0]));
344 assert_eq!(v.position_at(7, 1.5), Some([5.0, 0.0, 0.0]));
345 assert_eq!(v.position_at(7, 2.25), Some([15.0, 0.0, 0.0]));
346 assert_eq!(v.position_at(7, 9.0), Some([30.0, 0.0, 0.0]));
347 assert_eq!(v.position_at(7, 0.5), None);
349 assert_eq!(v.position_at(8, 2.0), None);
350 }
351
352 #[test]
353 fn a_quiet_entity_stands_still_until_the_tick_before_it_moves() {
354 let mut v = ViewHistory::new(20);
355 v.record(10, 0.5, false, &[s(1, [0, 0, 0])], &[], &[]);
356 v.record(15, 0.5, false, &[], &[s(1, [20, 0, 0])], &[]);
358 assert_eq!(v.position_at(1, 13.0), Some([0.0, 0.0, 0.0]));
359 assert_eq!(v.position_at(1, 14.0), Some([0.0, 0.0, 0.0]));
360 assert_eq!(v.position_at(1, 14.5), Some([5.0, 0.0, 0.0]));
361 assert_eq!(v.position_at(1, 15.0), Some([10.0, 0.0, 0.0]));
362 }
363
364 #[test]
365 fn a_despawn_ends_the_life_and_a_respawn_starts_another() {
366 let mut v = ViewHistory::new(20);
367 v.record(1, 1.0, false, &[s(4, [1, 1, 1])], &[], &[]);
368 v.record(3, 1.0, false, &[], &[], &[4]);
369 v.record(5, 1.0, false, &[s(4, [9, 9, 9])], &[], &[]);
370 assert_eq!(v.position_at(4, 2.9), Some([1.0, 1.0, 1.0]));
371 assert_eq!(v.position_at(4, 3.0), None);
372 assert_eq!(v.position_at(4, 4.0), None);
373 assert_eq!(v.position_at(4, 5.0), Some([9.0, 9.0, 9.0]));
374 }
375
376 #[test]
377 fn a_full_frame_ends_lives_it_does_not_list() {
378 let mut v = ViewHistory::new(20);
379 v.record(1, 1.0, true, &[s(1, [0, 0, 0]), s(2, [5, 0, 0])], &[], &[]);
380 v.record(4, 1.0, true, &[s(1, [3, 0, 0])], &[], &[]);
381 assert_eq!(v.position_at(2, 3.5), Some([5.0, 0.0, 0.0]));
382 assert_eq!(v.position_at(2, 4.0), None);
383 assert_eq!(v.position_at(1, 3.0), Some([0.0, 0.0, 0.0]));
385 assert_eq!(v.position_at(1, 4.0), Some([3.0, 0.0, 0.0]));
386 }
387
388 #[test]
389 fn a_component_change_without_a_move_adds_a_sample() {
390 let mut v = ViewHistory::new(20);
391 v.record(1, 1.0, false, &[s(1, [0, 0, 0])], &[], &[]);
392 let hp = Sent {
393 components_changed: true,
394 ..s(1, [0, 0, 0])
395 };
396 v.record(5, 1.0, false, &[], &[hp], &[]);
397 v.record(6, 1.0, false, &[], &[s(1, [10, 0, 0])], &[]);
398 assert_eq!(v.position_at(1, 5.0), Some([0.0, 0.0, 0.0]));
400 assert_eq!(v.position_at(1, 5.5), Some([5.0, 0.0, 0.0]));
401 }
402
403 #[test]
404 fn old_samples_and_lives_are_pruned() {
405 let mut v = ViewHistory::new(5);
406 v.record(1, 1.0, false, &[s(1, [0, 0, 0]), s(2, [0, 0, 0])], &[], &[]);
407 v.record(2, 1.0, false, &[], &[], &[2]);
408 for t in 3..=30 {
409 v.record(t, 1.0, false, &[], &[s(1, [t as i64, 0, 0])], &[]);
410 }
411 assert!(!v.lives.contains_key(&2), "an ended life long gone");
412 let samples = &v.lives[&1][0].samples;
413 assert!(
415 samples.len() <= 5 + PRUNE_EVERY as usize + 2,
416 "{} samples",
417 samples.len()
418 );
419 assert_eq!(v.position_at(1, 25.5), Some([25.5, 0.0, 0.0]));
421 }
422
423 #[test]
424 fn updates_in_any_order_land_on_their_entities() {
425 let mut v = ViewHistory::new(20);
426 v.record(
427 1,
428 1.0,
429 false,
430 &[s(1, [0, 0, 0]), s(2, [0, 0, 0]), s(3, [0, 0, 0])],
431 &[],
432 &[],
433 );
434 v.record(2, 1.0, false, &[], &[s(3, [3, 0, 0]), s(1, [1, 0, 0])], &[]);
435 v.record(3, 1.0, false, &[], &[s(1, [2, 0, 0]), s(3, [6, 0, 0])], &[]);
436 assert_eq!(v.position_at(1, 3.0), Some([2.0, 0.0, 0.0]));
437 assert_eq!(v.position_at(2, 3.0), Some([0.0, 0.0, 0.0]));
438 assert_eq!(v.position_at(3, 2.0), Some([3.0, 0.0, 0.0]));
439 assert_eq!(v.position_at(3, 3.0), Some([6.0, 0.0, 0.0]));
440 }
441
442 #[test]
443 fn near_finds_the_entities_within_the_radius_at_the_tick() {
444 let mut v = ViewHistory::new(20);
445 v.record(
446 1,
447 1.0,
448 false,
449 &[s(1, [0, 0, 0]), s(2, [100, 0, 0])],
450 &[],
451 &[],
452 );
453 v.record(2, 1.0, false, &[], &[s(2, [10, 0, 0])], &[]);
454 assert_eq!(
455 v.entities_near([0.0, 0.0, 0.0], 20.0, 1.0),
456 vec![(1, [0.0, 0.0, 0.0])]
457 );
458 assert_eq!(
459 v.entities_near([0.0, 0.0, 0.0], 20.0, 2.0),
460 vec![(1, [0.0, 0.0, 0.0]), (2, [10.0, 0.0, 0.0])]
461 );
462 }
463
464 #[test]
465 fn sparse_and_dense_updates_match_individual_lookups() {
466 let spawned: Vec<_> = (0..256).map(|id| s(id, [id as i64, 0, 0])).collect();
467 let mut history = ViewHistory::new(10);
468 history.record(1, 0.5, true, &spawned, &[], &[]);
469 let mut expected = history.clone();
470 for tick in 2..=40 {
471 let count = [1, 2, 31, 32, 33, 128, 256][tick as usize % 7];
473 let updated: Vec<_> = (0..count)
474 .map(|i| Sent {
475 components_changed: tick % 3 == 0,
476 ..s(256 - count + i, [i as i64, tick as i64, -1])
477 })
478 .chain([s(999, [0; 3])])
479 .collect();
480 let despawned = if tick == 5 { vec![255] } else { vec![] };
481 let respawned = if tick == 10 {
482 vec![s(255, [0; 3])]
483 } else {
484 vec![]
485 };
486 history.record(tick, 0.5, false, &respawned, &updated, &despawned);
487 let reversed: Vec<_> = updated.iter().rev().copied().collect();
489 expected.record(tick, 0.5, false, &respawned, &reversed, &despawned);
490 assert_eq!(
491 history.ids().collect::<Vec<_>>(),
492 expected.ids().collect::<Vec<_>>()
493 );
494 for (id, lives) in &history.lives {
495 let wanted = &expected.lives[id];
496 assert_eq!(lives.len(), wanted.len());
497 for (actual, expected) in lives.iter().zip(wanted) {
498 assert_eq!(actual.spawn_tick, expected.spawn_tick);
499 assert_eq!(actual.despawn_tick, expected.despawn_tick);
500 assert_eq!(actual.samples, expected.samples);
501 }
502 }
503 }
504 }
505
506 #[test]
507 fn nearby_queries_match_positions_across_entity_lifetimes() {
508 let mut history = ViewHistory::new(20);
509 history.record(1, 1.0, true, &[s(1, [0; 3]), s(2, [10, 0, 0])], &[], &[]);
510 history.record(3, 1.0, false, &[], &[s(2, [0, 10, 0])], &[1]);
511 history.record(5, 1.0, false, &[s(1, [0, 0, 10]), s(3, [20; 3])], &[], &[]);
512 for tick in [0.0, 1.0, 2.5, 3.0, 4.0, 5.0, 6.0] {
513 for radius in [0.0, 10.0, 30.0, -10.0, f64::INFINITY, f64::NAN] {
514 let expected: Vec<_> = history
515 .ids()
516 .filter_map(|id| {
517 let p = history.position_at(id, tick)?;
518 let distance = p[0] * p[0] + p[1] * p[1] + p[2] * p[2];
519 (distance <= radius * radius).then_some((id, p))
520 })
521 .collect();
522 assert_eq!(history.entities_near([0.0; 3], radius, tick), expected);
523 }
524 }
525 }
526}