use std::collections::{BTreeMap, VecDeque};
use crate::EntityId;
#[derive(Debug, Clone, Copy, PartialEq)]
struct Sample {
tick: u64,
pos: [f64; 3],
components: u64,
}
#[derive(Debug, Clone)]
struct Life {
spawn_tick: u64,
despawn_tick: Option<u64>,
samples: VecDeque<Sample>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Sent {
pub id: EntityId,
pub q: [i64; 3],
pub components_changed: bool,
}
#[derive(Debug, Clone)]
pub struct ViewHistory {
keep_ticks: u64,
lives: BTreeMap<EntityId, Vec<Life>>,
last_tick: Option<u64>,
pruned_at: u64,
}
const PRUNE_EVERY: u64 = 8;
impl ViewHistory {
pub fn new(keep_ticks: u64) -> Self {
Self {
keep_ticks,
lives: BTreeMap::new(),
last_tick: None,
pruned_at: 0,
}
}
pub fn keep_ticks(&self) -> u64 {
self.keep_ticks
}
pub fn last_tick(&self) -> Option<u64> {
self.last_tick
}
pub fn clear(&mut self) {
self.lives.clear();
self.last_tick = None;
self.pruned_at = 0;
}
pub fn record(
&mut self,
tick: u64,
precision: f32,
full: bool,
spawned: &[Sent],
updated: &[Sent],
despawned: &[EntityId],
) {
if self.last_tick.is_some_and(|last| tick < last) {
self.clear();
}
self.last_tick = Some(tick);
let p = precision as f64;
let pos = |q: [i64; 3]| [q[0] as f64 * p, q[1] as f64 * p, q[2] as f64 * p];
if full {
let present: std::collections::HashSet<EntityId> =
spawned.iter().map(|s| s.id).collect();
for (id, lives) in self.lives.iter_mut() {
if let Some(open) = lives.last_mut().filter(|l| l.despawn_tick.is_none()) {
if !present.contains(id) {
open.despawn_tick = Some(tick);
}
}
}
for s in spawned {
match self.open_life(s.id) {
Some(life) => Self::push(life, tick, pos(s.q), s.components_changed),
None => self.spawn(s.id, tick, pos(s.q)),
}
}
} else {
for id in despawned {
if let Some(open) = self.open_life(*id) {
open.despawn_tick = Some(tick);
}
}
for s in spawned {
self.spawn(s.id, tick, pos(s.q));
}
if updated.windows(2).all(|w| w[0].id < w[1].id) {
let mut lives = self.lives.iter_mut().peekable();
for s in updated {
while lives.next_if(|(id, _)| **id < s.id).is_some() {}
if let Some((_, l)) = lives.next_if(|(id, _)| **id == s.id) {
if let Some(life) = l.last_mut().filter(|l| l.despawn_tick.is_none()) {
Self::push(life, tick, pos(s.q), s.components_changed);
}
}
}
} else {
for s in updated {
if let Some(life) = self.open_life(s.id) {
Self::push(life, tick, pos(s.q), s.components_changed);
}
}
}
}
if tick >= self.pruned_at + PRUNE_EVERY || tick < self.pruned_at {
self.prune(tick);
self.pruned_at = tick;
}
}
pub fn position_at(&self, id: EntityId, render_tick: f64) -> Option<[f64; 3]> {
let lives = self.lives.get(&id)?;
let life = lives
.iter()
.find(|l| l.despawn_tick.is_none_or(|d| (d as f64) > render_tick))?;
if (life.spawn_tick as f64) > render_tick {
return None;
}
Some(place(&life.samples, render_tick))
}
pub fn entities_near(
&self,
center: [f64; 3],
radius: f64,
render_tick: f64,
) -> Vec<(EntityId, [f64; 3])> {
let r2 = radius * radius;
let mut out = Vec::new();
for id in self.lives.keys() {
if let Some(p) = self.position_at(*id, render_tick) {
let d = [p[0] - center[0], p[1] - center[1], p[2] - center[2]];
if d[0] * d[0] + d[1] * d[1] + d[2] * d[2] <= r2 {
out.push((*id, p));
}
}
}
out
}
pub fn ids(&self) -> impl Iterator<Item = EntityId> + '_ {
self.lives.keys().copied()
}
fn open_life(&mut self, id: EntityId) -> Option<&mut Life> {
self.lives
.get_mut(&id)?
.last_mut()
.filter(|l| l.despawn_tick.is_none())
}
fn spawn(&mut self, id: EntityId, tick: u64, pos: [f64; 3]) {
let lives = self.lives.entry(id).or_default();
if let Some(open) = lives.last_mut().filter(|l| l.despawn_tick.is_none()) {
open.despawn_tick = Some(tick);
}
let mut samples = VecDeque::with_capacity(4);
samples.push_back(Sample {
tick,
pos,
components: 0,
});
lives.push(Life {
spawn_tick: tick,
despawn_tick: None,
samples,
});
}
fn push(life: &mut Life, tick: u64, pos: [f64; 3], components_changed: bool) {
let samples = &mut life.samples;
let last = *samples.back().expect("a life has a sample");
let moved = last.pos != pos;
if !moved && !components_changed {
return;
}
let components = if components_changed {
last.components + 1
} else {
last.components
};
if last.tick == tick {
*samples.back_mut().expect("checked") = Sample {
tick,
pos,
components,
};
return;
}
if moved && last.tick + 1 < tick {
samples.push_back(Sample {
tick: tick - 1,
..last
});
}
samples.push_back(Sample {
tick,
pos,
components,
});
}
fn prune(&mut self, tick: u64) {
let oldest = tick.saturating_sub(self.keep_ticks);
self.lives.retain(|_, lives| {
lives.retain(|l| l.despawn_tick.is_none_or(|d| d > oldest));
for life in lives.iter_mut() {
while life.samples.len() > 1 && life.samples[1].tick <= oldest {
life.samples.pop_front();
}
}
!lives.is_empty()
});
}
}
fn place(samples: &VecDeque<Sample>, render_tick: f64) -> [f64; 3] {
let mut i = samples.len() - 1;
while i > 0 && (samples[i].tick as f64) > render_tick {
i -= 1;
}
let from = samples[i];
let to = samples.get(i + 1).copied().unwrap_or(from);
let t = if to.tick == from.tick {
0.0
} else {
((render_tick - from.tick as f64) / (to.tick as f64 - from.tick as f64)).clamp(0.0, 1.0)
};
let d = [
to.pos[0] - from.pos[0],
to.pos[1] - from.pos[1],
to.pos[2] - from.pos[2],
];
[
from.pos[0] + d[0] * t,
from.pos[1] + d[1] * t,
from.pos[2] + d[2] * t,
]
}
#[cfg(test)]
mod tests {
use super::*;
fn s(id: EntityId, q: [i64; 3]) -> Sent {
Sent {
id,
q,
components_changed: false,
}
}
#[test]
fn an_entity_is_placed_between_samples_and_held_after_the_last() {
let mut v = ViewHistory::new(20);
v.record(1, 1.0, true, &[s(7, [0, 0, 0])], &[], &[]);
v.record(2, 1.0, false, &[], &[s(7, [10, 0, 0])], &[]);
v.record(3, 1.0, false, &[], &[s(7, [30, 0, 0])], &[]);
assert_eq!(v.position_at(7, 1.0), Some([0.0, 0.0, 0.0]));
assert_eq!(v.position_at(7, 1.5), Some([5.0, 0.0, 0.0]));
assert_eq!(v.position_at(7, 2.25), Some([15.0, 0.0, 0.0]));
assert_eq!(v.position_at(7, 9.0), Some([30.0, 0.0, 0.0]));
assert_eq!(v.position_at(7, 0.5), None);
assert_eq!(v.position_at(8, 2.0), None);
}
#[test]
fn a_quiet_entity_stands_still_until_the_tick_before_it_moves() {
let mut v = ViewHistory::new(20);
v.record(10, 0.5, false, &[s(1, [0, 0, 0])], &[], &[]);
v.record(15, 0.5, false, &[], &[s(1, [20, 0, 0])], &[]);
assert_eq!(v.position_at(1, 13.0), Some([0.0, 0.0, 0.0]));
assert_eq!(v.position_at(1, 14.0), Some([0.0, 0.0, 0.0]));
assert_eq!(v.position_at(1, 14.5), Some([5.0, 0.0, 0.0]));
assert_eq!(v.position_at(1, 15.0), Some([10.0, 0.0, 0.0]));
}
#[test]
fn a_despawn_ends_the_life_and_a_respawn_starts_another() {
let mut v = ViewHistory::new(20);
v.record(1, 1.0, false, &[s(4, [1, 1, 1])], &[], &[]);
v.record(3, 1.0, false, &[], &[], &[4]);
v.record(5, 1.0, false, &[s(4, [9, 9, 9])], &[], &[]);
assert_eq!(v.position_at(4, 2.9), Some([1.0, 1.0, 1.0]));
assert_eq!(v.position_at(4, 3.0), None);
assert_eq!(v.position_at(4, 4.0), None);
assert_eq!(v.position_at(4, 5.0), Some([9.0, 9.0, 9.0]));
}
#[test]
fn a_full_frame_ends_lives_it_does_not_list() {
let mut v = ViewHistory::new(20);
v.record(1, 1.0, true, &[s(1, [0, 0, 0]), s(2, [5, 0, 0])], &[], &[]);
v.record(4, 1.0, true, &[s(1, [3, 0, 0])], &[], &[]);
assert_eq!(v.position_at(2, 3.5), Some([5.0, 0.0, 0.0]));
assert_eq!(v.position_at(2, 4.0), None);
assert_eq!(v.position_at(1, 3.0), Some([0.0, 0.0, 0.0]));
assert_eq!(v.position_at(1, 4.0), Some([3.0, 0.0, 0.0]));
}
#[test]
fn a_component_change_without_a_move_adds_a_sample() {
let mut v = ViewHistory::new(20);
v.record(1, 1.0, false, &[s(1, [0, 0, 0])], &[], &[]);
let hp = Sent {
components_changed: true,
..s(1, [0, 0, 0])
};
v.record(5, 1.0, false, &[], &[hp], &[]);
v.record(6, 1.0, false, &[], &[s(1, [10, 0, 0])], &[]);
assert_eq!(v.position_at(1, 5.0), Some([0.0, 0.0, 0.0]));
assert_eq!(v.position_at(1, 5.5), Some([5.0, 0.0, 0.0]));
}
#[test]
fn old_samples_and_lives_are_pruned() {
let mut v = ViewHistory::new(5);
v.record(1, 1.0, false, &[s(1, [0, 0, 0]), s(2, [0, 0, 0])], &[], &[]);
v.record(2, 1.0, false, &[], &[], &[2]);
for t in 3..=30 {
v.record(t, 1.0, false, &[], &[s(1, [t as i64, 0, 0])], &[]);
}
assert!(!v.lives.contains_key(&2), "an ended life long gone");
let samples = &v.lives[&1][0].samples;
assert!(
samples.len() <= 5 + PRUNE_EVERY as usize + 2,
"{} samples",
samples.len()
);
assert_eq!(v.position_at(1, 25.5), Some([25.5, 0.0, 0.0]));
}
#[test]
fn updates_in_any_order_land_on_their_entities() {
let mut v = ViewHistory::new(20);
v.record(
1,
1.0,
false,
&[s(1, [0, 0, 0]), s(2, [0, 0, 0]), s(3, [0, 0, 0])],
&[],
&[],
);
v.record(2, 1.0, false, &[], &[s(3, [3, 0, 0]), s(1, [1, 0, 0])], &[]);
v.record(3, 1.0, false, &[], &[s(1, [2, 0, 0]), s(3, [6, 0, 0])], &[]);
assert_eq!(v.position_at(1, 3.0), Some([2.0, 0.0, 0.0]));
assert_eq!(v.position_at(2, 3.0), Some([0.0, 0.0, 0.0]));
assert_eq!(v.position_at(3, 2.0), Some([3.0, 0.0, 0.0]));
assert_eq!(v.position_at(3, 3.0), Some([6.0, 0.0, 0.0]));
}
#[test]
fn near_finds_the_entities_within_the_radius_at_the_tick() {
let mut v = ViewHistory::new(20);
v.record(
1,
1.0,
false,
&[s(1, [0, 0, 0]), s(2, [100, 0, 0])],
&[],
&[],
);
v.record(2, 1.0, false, &[], &[s(2, [10, 0, 0])], &[]);
assert_eq!(
v.entities_near([0.0, 0.0, 0.0], 20.0, 1.0),
vec![(1, [0.0, 0.0, 0.0])]
);
assert_eq!(
v.entities_near([0.0, 0.0, 0.0], 20.0, 2.0),
vec![(1, [0.0, 0.0, 0.0]), (2, [10.0, 0.0, 0.0])]
);
}
}