use std::collections::HashSet;
use indexmap::IndexMap;
use crate::config::{BlockClass, FieldValue, Interp, InterpolationConfig, SnapshotConfig};
use crate::physics::{lerp, lerp_angle};
use super::unpack::{BlockData, DecodedCamera, DecodedSnapshot};
const OFFSET_SMOOTHING: f64 = 0.1;
const SEQ_DEDUP_WINDOW: u32 = 128;
const MAX_BUFFER_FRAMES: usize = 256;
#[derive(Clone)]
pub struct FrameData {
pub snapshot: DecodedSnapshot,
pub camera: Option<DecodedCamera>,
}
struct BufferedFrame {
seq: u32,
server_time: f64,
data: FrameData,
issued: bool,
}
pub struct InterpolatedRow {
pub id: u32,
pub fields: Vec<FieldValue>,
}
#[derive(Default)]
pub struct InterpolatedGame {
pub blocks: IndexMap<String, Vec<InterpolatedRow>>,
}
pub struct SampleResult {
pub frames: Vec<FrameData>,
pub game: Option<InterpolatedGame>,
pub camera: Option<[f32; 2]>,
}
pub struct Interpolator {
delay: f64,
max_frame_age: f64,
snapshot_cfg: SnapshotConfig,
frames: Vec<BufferedFrame>,
offset_ema: Option<f64>,
last_render_time: Option<f64>,
pending_late: Vec<FrameData>,
issued_seqs: HashSet<u32>,
}
impl Interpolator {
pub fn new(cfg: &InterpolationConfig, snapshot_cfg: SnapshotConfig) -> Self {
Self {
delay: cfg.delay,
max_frame_age: cfg.max_frame_age,
snapshot_cfg,
frames: Vec::new(),
offset_ema: None,
last_render_time: None,
pending_late: Vec::new(),
issued_seqs: HashSet::new(),
}
}
pub fn offset(&self) -> Option<f64> {
self.offset_ema
}
pub fn push(&mut self, data: FrameData, server_time: f64, local_now: f64, seq: u32) {
if self.issued_seqs.contains(&seq) {
return;
}
let mut index = self.frames.len();
while index > 0 && self.frames[index - 1].seq >= seq {
if self.frames[index - 1].seq == seq {
return;
}
index -= 1;
}
let offset = server_time - local_now;
match &mut self.offset_ema {
None => self.offset_ema = Some(offset),
Some(ema) => *ema += (offset - *ema) * OFFSET_SMOOTHING,
}
if let Some(last) = self.last_render_time
&& server_time <= last
{
self.issued_seqs.insert(seq);
self.pending_late.push(data);
return;
}
self.frames.insert(
index,
BufferedFrame {
seq,
server_time,
data,
issued: false,
},
);
let newest_time = self.frames[self.frames.len() - 1].server_time;
let min_time = newest_time - self.max_frame_age;
while self.frames.len() > 2 && self.frames[0].server_time < min_time {
self.frames.remove(0);
}
while self.frames.len() > MAX_BUFFER_FRAMES {
self.frames.remove(0);
}
}
pub fn sample(&mut self, local_now: f64) -> SampleResult {
let mut frames = std::mem::take(&mut self.pending_late);
let Some(offset) = self.offset_ema else {
return SampleResult {
frames,
game: None,
camera: None,
};
};
if self.frames.is_empty() {
return SampleResult {
frames,
game: None,
camera: None,
};
}
let render_time = local_now + offset - self.delay;
self.last_render_time = Some(render_time);
let mut index_a: isize = -1;
for (i, frame) in self.frames.iter().enumerate() {
if frame.server_time <= render_time {
index_a = i as isize;
} else {
break;
}
}
if index_a == -1 {
return SampleResult {
frames,
game: None,
camera: None,
};
}
let index_a = index_a as usize;
for frame in self.frames.drain(0..index_a) {
if !frame.issued {
self.issued_seqs.insert(frame.seq);
frames.push(frame.data);
}
}
if !self.frames[0].issued {
self.frames[0].issued = true;
self.issued_seqs.insert(self.frames[0].seq);
frames.push(self.frames[0].data.clone());
}
self.prune_issued_seqs();
let frame_a = &self.frames[0];
let Some(frame_b) = self.frames.get(1) else {
let game = interpolate_game(
&frame_a.data.snapshot,
&frame_a.data.snapshot,
0.0,
&self.snapshot_cfg,
);
let camera = strip_camera(frame_a.data.camera.as_ref());
return SampleResult {
frames,
game: Some(game),
camera,
};
};
let alpha = ((render_time - frame_a.server_time)
/ (frame_b.server_time - frame_a.server_time))
.clamp(0.0, 1.0) as f32;
let game = interpolate_game(
&frame_a.data.snapshot,
&frame_b.data.snapshot,
alpha,
&self.snapshot_cfg,
);
let camera = interpolate_camera(
frame_a.data.camera.as_ref(),
frame_b.data.camera.as_ref(),
alpha,
);
SampleResult {
frames,
game: Some(game),
camera,
}
}
pub fn debug_json(&self) -> serde_json::Value {
let last = self.frames.last();
serde_json::json!({
"delay": self.delay,
"maxFrameAge": self.max_frame_age,
"buffered": self.frames.len(),
"seqWindow": self.frames.first().map(|frame| [frame.seq, self.frames[self.frames.len() - 1].seq]),
"lastFrame": last.map(|frame| serde_json::json!({
"seq": frame.seq,
"serverTime": frame.server_time,
"issued": frame.issued,
})),
"offset": self.offset_ema,
"lastRenderTime": self.last_render_time,
"pendingLate": self.pending_late.len(),
"issuedSeqs": self.issued_seqs.len(),
})
}
pub fn reset(&mut self) {
self.frames.clear();
self.offset_ema = None;
self.last_render_time = None;
self.pending_late.clear();
self.issued_seqs.clear();
}
fn prune_issued_seqs(&mut self) {
let min_seq = self.frames[0].seq.saturating_sub(SEQ_DEDUP_WINDOW);
self.issued_seqs.retain(|seq| *seq >= min_seq);
}
}
fn interp_field(interp: Interp, a: FieldValue, b: FieldValue, alpha: f32) -> FieldValue {
match (interp, a, b) {
(Interp::Lerp, FieldValue::F32(a), FieldValue::F32(b)) => FieldValue::F32(lerp(a, b, alpha)),
(Interp::LerpAngle, FieldValue::F32(a), FieldValue::F32(b)) => {
FieldValue::F32(lerp_angle(a, b, alpha))
}
_ => a,
}
}
fn interpolate_row(
schema_fields: &[crate::config::FieldSchema],
fields_a: &[FieldValue],
fields_b: &[FieldValue],
alpha: f32,
) -> Vec<FieldValue> {
fields_a
.iter()
.zip(fields_b)
.enumerate()
.map(|(i, (a, b))| {
let interp = schema_fields.get(i).map_or(Interp::Discrete, |f| f.interp);
interp_field(interp, *a, *b, alpha)
})
.collect()
}
fn interpolate_game(
a: &DecodedSnapshot,
b: &DecodedSnapshot,
alpha: f32,
cfg: &SnapshotConfig,
) -> InterpolatedGame {
let mut game = InterpolatedGame::default();
for block in &a.blocks {
let Some(block_b) = b.block_by_key(&block.key) else {
continue;
};
let Some(schema) = cfg.keys.get(&block.key) else {
continue;
};
if schema.class != BlockClass::Hot {
continue;
}
let rows: Vec<InterpolatedRow> = match (&block.data, block_b) {
(BlockData::Indexed8(items_a), BlockData::Indexed8(items_b)) => items_a
.iter()
.filter_map(|(id, row_a)| {
let (Some(row_a), Some(Some(row_b))) = (row_a, items_b.get(id)) else {
return None;
};
Some(InterpolatedRow {
id: *id as u32,
fields: interpolate_row(&schema.fields, row_a, row_b, alpha),
})
})
.collect(),
(BlockData::IndexedNoNull8(items_a), BlockData::IndexedNoNull8(items_b)) => items_a
.iter()
.filter_map(|(index, fields_a)| {
let fields_b = items_b.get(index)?;
Some(InterpolatedRow {
id: *index as u32,
fields: interpolate_row(&schema.fields, fields_a, fields_b, alpha),
})
})
.collect(),
_ => continue,
};
if !rows.is_empty() {
game.blocks.insert(block.key.clone(), rows);
}
}
game
}
fn interpolate_camera(
a: Option<&DecodedCamera>,
b: Option<&DecodedCamera>,
alpha: f32,
) -> Option<[f32; 2]> {
match (a, b) {
(Some(a), Some(b)) => Some([lerp(a.x, b.x, alpha), lerp(a.y, b.y, alpha)]),
_ => strip_camera(a),
}
}
fn strip_camera(camera: Option<&DecodedCamera>) -> Option<[f32; 2]> {
camera.map(|camera| [camera.x, camera.y])
}
#[cfg(test)]
mod tests {
use indexmap::IndexMap;
use super::*;
use crate::config::test_support::full_snapshot_config;
use super::super::unpack::DecodedBlock;
const DELAY: f64 = 100.0;
fn make() -> Interpolator {
Interpolator::new(
&InterpolationConfig {
delay: DELAY,
max_frame_age: 1000.0,
},
full_snapshot_config(3, 5),
)
}
fn tank_row(x: f32, angle: f32) -> Vec<FieldValue> {
vec![
FieldValue::F32(x),
FieldValue::F32(0.0),
FieldValue::F32(angle),
FieldValue::F32(0.0),
FieldValue::F32(0.0),
FieldValue::F32(0.0),
FieldValue::F32(0.0),
FieldValue::U8(3),
FieldValue::U8(2),
FieldValue::U8(1),
]
}
fn tank_frame(entries: &[(u8, Option<Vec<FieldValue>>)]) -> FrameData {
let mut items = IndexMap::new();
for (id, row) in entries {
items.insert(*id, row.clone());
}
FrameData {
snapshot: DecodedSnapshot {
blocks: vec![DecodedBlock {
key: "m1".to_string(),
key_id: 1,
data: BlockData::Indexed8(items),
}],
},
camera: None,
}
}
fn push_zero_offset(i: &mut Interpolator, frame: FrameData, server_time: f64, seq: u32) {
i.push(frame, server_time, server_time, seq);
}
fn tanks(game: &InterpolatedGame) -> &[InterpolatedRow] {
game.blocks.get("m1").map_or(&[], Vec::as_slice)
}
fn f32_at(row: &InterpolatedRow, i: usize) -> f32 {
match row.fields[i] {
FieldValue::F32(v) => v,
_ => panic!("поле {i} не F32"),
}
}
fn u8_at(row: &InterpolatedRow, i: usize) -> u8 {
match row.fields[i] {
FieldValue::U8(v) => v,
_ => panic!("поле {i} не U8"),
}
}
#[test]
fn empty_buffer_returns_nothing() {
let mut i = make();
let result = i.sample(0.0);
assert!(result.frames.is_empty());
assert!(result.game.is_none());
assert!(result.camera.is_none());
}
#[test]
fn render_time_before_first_frame_returns_nothing() {
let mut i = make();
push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
let result = i.sample(1000.0);
assert!(result.frames.is_empty());
assert!(result.game.is_none());
}
#[test]
fn positions_lerp_between_frames() {
let mut i = make();
push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(100.0, 0.0)))]), 1100.0, 2);
let result = i.sample(1150.0);
let game = result.game.unwrap();
let rows = tanks(&game);
assert_eq!(rows.len(), 1);
assert_eq!(f32_at(&rows[0], 0), 50.0);
assert_eq!(rows[0].id, 1);
}
#[test]
fn angles_lerp_shortest_path() {
use std::f32::consts::PI;
let mut i = make();
push_zero_offset(
&mut i,
tank_frame(&[(1, Some(tank_row(0.0, PI - 0.1)))]),
1000.0,
1,
);
push_zero_offset(
&mut i,
tank_frame(&[(1, Some(tank_row(0.0, -PI + 0.1)))]),
1100.0,
2,
);
let result = i.sample(1150.0);
let game = result.game.unwrap();
let angle = f32_at(&tanks(&game)[0], 2);
assert!((angle.abs() - PI).abs() < 0.01);
}
#[test]
fn discrete_fields_come_from_frame_a() {
let mut i = make();
let mut row_b = tank_row(100.0, 0.0);
row_b[7] = FieldValue::U8(1);
push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
push_zero_offset(&mut i, tank_frame(&[(1, Some(row_b))]), 1100.0, 2);
let result = i.sample(1150.0);
let game = result.game.unwrap();
assert_eq!(u8_at(&tanks(&game)[0], 7), 3);
}
#[test]
fn null_and_missing_tanks_are_not_interpolated() {
let mut i = make();
push_zero_offset(
&mut i,
tank_frame(&[(1, Some(tank_row(0.0, 0.0))), (2, Some(tank_row(5.0, 0.0)))]),
1000.0,
1,
);
push_zero_offset(&mut i, tank_frame(&[(1, None)]), 1100.0, 2);
let result = i.sample(1150.0);
assert!(tanks(&result.game.unwrap()).is_empty());
}
#[test]
fn frames_are_issued_exactly_once() {
let mut i = make();
push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(10.0, 0.0)))]), 1100.0, 2);
assert_eq!(i.sample(1150.0).frames.len(), 1); assert_eq!(i.sample(1151.0).frames.len(), 0); assert_eq!(i.sample(1250.0).frames.len(), 1); }
#[test]
fn hold_on_last_frame_without_extrapolation() {
let mut i = make();
push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(50.0, 0.0)))]), 1000.0, 1);
let result = i.sample(1500.0);
let game = result.game.unwrap();
assert_eq!(f32_at(&tanks(&game)[0], 0), 50.0);
}
#[test]
fn out_of_order_insert_and_duplicates() {
let mut i = make();
push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(100.0, 0.0)))]), 1200.0, 3);
push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(50.0, 0.0)))]), 1100.0, 2);
push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(999.0, 0.0)))]), 1100.0, 2);
let result = i.sample(1250.0);
let game = result.game.unwrap();
assert_eq!(f32_at(&tanks(&game)[0], 0), 75.0);
assert_eq!(result.frames.len(), 2); }
#[test]
fn late_frame_events_are_issued_immediately() {
let mut i = make();
push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(20.0, 0.0)))]), 1200.0, 3);
i.sample(1250.0);
push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(10.0, 0.0)))]), 1100.0, 2);
let result = i.sample(1251.0);
assert_eq!(result.frames.len(), 1);
push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(10.0, 0.0)))]), 1100.0, 2);
assert!(i.sample(1252.0).frames.is_empty());
}
#[test]
fn camera_lerp_and_strip() {
let mut i = make();
let frame = |x: f32| FrameData {
snapshot: DecodedSnapshot::default(),
camera: Some(DecodedCamera {
x,
y: 0.0,
force_reset: true,
shake: None,
}),
};
push_zero_offset(&mut i, frame(0.0), 1000.0, 1);
push_zero_offset(&mut i, frame(100.0), 1100.0, 2);
let result = i.sample(1150.0);
assert_eq!(result.camera, Some([50.0, 0.0]));
}
#[test]
fn offset_ema_smoothing() {
let mut i = make();
i.push(tank_frame(&[]), 1000.0, 500.0, 1); assert_eq!(i.offset(), Some(500.0));
i.push(tank_frame(&[]), 1100.0, 500.0, 2); assert_eq!(i.offset(), Some(510.0));
}
#[test]
fn old_frames_are_pruned_by_age() {
let mut i = make();
push_zero_offset(&mut i, tank_frame(&[]), 1000.0, 1);
push_zero_offset(&mut i, tank_frame(&[]), 1100.0, 2);
push_zero_offset(&mut i, tank_frame(&[]), 2500.0, 3);
let result = i.sample(2500.0);
assert_eq!(result.frames.len(), 1);
}
#[test]
fn reset_clears_everything() {
let mut i = make();
push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
i.reset();
assert!(i.offset().is_none());
let result = i.sample(2000.0);
assert!(result.frames.is_empty());
assert!(result.game.is_none());
push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(5.0, 0.0)))]), 3000.0, 1);
assert_eq!(i.sample(3200.0).frames.len(), 1);
}
}