1use std::collections::HashSet;
7
8use indexmap::IndexMap;
9
10use crate::config::{BlockClass, FieldValue, Interp, InterpolationConfig, SnapshotConfig};
11use crate::physics::{lerp, lerp_angle};
12
13use super::unpack::{BlockData, DecodedCamera, DecodedSnapshot};
14
15const OFFSET_SMOOTHING: f64 = 0.1;
17
18const SEQ_DEDUP_WINDOW: u32 = 128;
20
21const MAX_BUFFER_FRAMES: usize = 256;
24
25#[derive(Clone)]
27pub struct FrameData {
28 pub snapshot: DecodedSnapshot,
29 pub camera: Option<DecodedCamera>,
30}
31
32struct BufferedFrame {
33 seq: u32,
34 server_time: f64,
35 data: FrameData,
36 issued: bool,
37}
38
39pub struct InterpolatedRow {
43 pub id: u32,
44 pub fields: Vec<FieldValue>,
45}
46
47#[derive(Default)]
52pub struct InterpolatedGame {
53 pub blocks: IndexMap<String, Vec<InterpolatedRow>>,
54}
55
56pub struct SampleResult {
58 pub frames: Vec<FrameData>,
60 pub game: Option<InterpolatedGame>,
61 pub camera: Option<[f32; 2]>,
63}
64
65pub struct Interpolator {
66 delay: f64,
67 max_frame_age: f64,
68 snapshot_cfg: SnapshotConfig,
69 frames: Vec<BufferedFrame>,
70 offset_ema: Option<f64>,
71 last_render_time: Option<f64>,
72 pending_late: Vec<FrameData>,
73 issued_seqs: HashSet<u32>,
74}
75
76impl Interpolator {
77 pub fn new(cfg: &InterpolationConfig, snapshot_cfg: SnapshotConfig) -> Self {
78 Self {
79 delay: cfg.delay,
80 max_frame_age: cfg.max_frame_age,
81 snapshot_cfg,
82 frames: Vec::new(),
83 offset_ema: None,
84 last_render_time: None,
85 pending_late: Vec::new(),
86 issued_seqs: HashSet::new(),
87 }
88 }
89
90 pub fn offset(&self) -> Option<f64> {
92 self.offset_ema
93 }
94
95 pub fn push(&mut self, data: FrameData, server_time: f64, local_now: f64, seq: u32) {
97 if self.issued_seqs.contains(&seq) {
98 return;
99 }
100
101 let mut index = self.frames.len();
104
105 while index > 0 && self.frames[index - 1].seq >= seq {
106 if self.frames[index - 1].seq == seq {
107 return;
108 }
109
110 index -= 1;
111 }
112
113 let offset = server_time - local_now;
114
115 match &mut self.offset_ema {
116 None => self.offset_ema = Some(offset),
117 Some(ema) => *ema += (offset - *ema) * OFFSET_SMOOTHING,
118 }
119
120 if let Some(last) = self.last_render_time
123 && server_time <= last
124 {
125 self.issued_seqs.insert(seq);
126 self.pending_late.push(data);
127
128 return;
129 }
130
131 self.frames.insert(
132 index,
133 BufferedFrame {
134 seq,
135 server_time,
136 data,
137 issued: false,
138 },
139 );
140
141 let newest_time = self.frames[self.frames.len() - 1].server_time;
143 let min_time = newest_time - self.max_frame_age;
144
145 while self.frames.len() > 2 && self.frames[0].server_time < min_time {
146 self.frames.remove(0);
147 }
148
149 while self.frames.len() > MAX_BUFFER_FRAMES {
150 self.frames.remove(0);
151 }
152 }
153
154 pub fn sample(&mut self, local_now: f64) -> SampleResult {
156 let mut frames = std::mem::take(&mut self.pending_late);
158
159 let Some(offset) = self.offset_ema else {
160 return SampleResult {
161 frames,
162 game: None,
163 camera: None,
164 };
165 };
166
167 if self.frames.is_empty() {
168 return SampleResult {
169 frames,
170 game: None,
171 camera: None,
172 };
173 }
174
175 let render_time = local_now + offset - self.delay;
176
177 self.last_render_time = Some(render_time);
178
179 let mut index_a: isize = -1;
181
182 for (i, frame) in self.frames.iter().enumerate() {
183 if frame.server_time <= render_time {
184 index_a = i as isize;
185 } else {
186 break;
187 }
188 }
189
190 if index_a == -1 {
192 return SampleResult {
193 frames,
194 game: None,
195 camera: None,
196 };
197 }
198
199 let index_a = index_a as usize;
200
201 for frame in self.frames.drain(0..index_a) {
204 if !frame.issued {
205 self.issued_seqs.insert(frame.seq);
206 frames.push(frame.data);
207 }
208 }
209
210 if !self.frames[0].issued {
211 self.frames[0].issued = true;
212 self.issued_seqs.insert(self.frames[0].seq);
213 frames.push(self.frames[0].data.clone());
214 }
215
216 self.prune_issued_seqs();
217
218 let frame_a = &self.frames[0];
219
220 let Some(frame_b) = self.frames.get(1) else {
222 let game = interpolate_game(
223 &frame_a.data.snapshot,
224 &frame_a.data.snapshot,
225 0.0,
226 &self.snapshot_cfg,
227 );
228 let camera = strip_camera(frame_a.data.camera.as_ref());
229
230 return SampleResult {
231 frames,
232 game: Some(game),
233 camera,
234 };
235 };
236
237 let alpha = ((render_time - frame_a.server_time)
238 / (frame_b.server_time - frame_a.server_time))
239 .clamp(0.0, 1.0) as f32;
240
241 let game = interpolate_game(
242 &frame_a.data.snapshot,
243 &frame_b.data.snapshot,
244 alpha,
245 &self.snapshot_cfg,
246 );
247 let camera = interpolate_camera(
248 frame_a.data.camera.as_ref(),
249 frame_b.data.camera.as_ref(),
250 alpha,
251 );
252
253 SampleResult {
254 frames,
255 game: Some(game),
256 camera,
257 }
258 }
259
260 pub fn debug_json(&self) -> serde_json::Value {
264 let last = self.frames.last();
265
266 serde_json::json!({
267 "delay": self.delay,
268 "maxFrameAge": self.max_frame_age,
269 "buffered": self.frames.len(),
270 "seqWindow": self.frames.first().map(|frame| [frame.seq, self.frames[self.frames.len() - 1].seq]),
271 "lastFrame": last.map(|frame| serde_json::json!({
272 "seq": frame.seq,
273 "serverTime": frame.server_time,
274 "issued": frame.issued,
275 })),
276 "offset": self.offset_ema,
277 "lastRenderTime": self.last_render_time,
278 "pendingLate": self.pending_late.len(),
279 "issuedSeqs": self.issued_seqs.len(),
280 })
281 }
282
283 pub fn reset(&mut self) {
285 self.frames.clear();
286 self.offset_ema = None;
287 self.last_render_time = None;
288 self.pending_late.clear();
289 self.issued_seqs.clear();
290 }
291
292 fn prune_issued_seqs(&mut self) {
294 let min_seq = self.frames[0].seq.saturating_sub(SEQ_DEDUP_WINDOW);
295
296 self.issued_seqs.retain(|seq| *seq >= min_seq);
297 }
298}
299
300fn interp_field(interp: Interp, a: FieldValue, b: FieldValue, alpha: f32) -> FieldValue {
306 match (interp, a, b) {
307 (Interp::Lerp, FieldValue::F32(a), FieldValue::F32(b)) => FieldValue::F32(lerp(a, b, alpha)),
308 (Interp::LerpAngle, FieldValue::F32(a), FieldValue::F32(b)) => {
309 FieldValue::F32(lerp_angle(a, b, alpha))
310 }
311 _ => a,
312 }
313}
314
315fn interpolate_row(
321 schema_fields: &[crate::config::FieldSchema],
322 fields_a: &[FieldValue],
323 fields_b: &[FieldValue],
324 alpha: f32,
325) -> Vec<FieldValue> {
326 fields_a
327 .iter()
328 .zip(fields_b)
329 .enumerate()
330 .map(|(i, (a, b))| {
331 let interp = schema_fields.get(i).map_or(Interp::Discrete, |f| f.interp);
332
333 interp_field(interp, *a, *b, alpha)
334 })
335 .collect()
336}
337
338fn interpolate_game(
345 a: &DecodedSnapshot,
346 b: &DecodedSnapshot,
347 alpha: f32,
348 cfg: &SnapshotConfig,
349) -> InterpolatedGame {
350 let mut game = InterpolatedGame::default();
351
352 for block in &a.blocks {
353 let Some(block_b) = b.block_by_key(&block.key) else {
354 continue;
355 };
356
357 let Some(schema) = cfg.keys.get(&block.key) else {
358 continue;
359 };
360
361 if schema.class != BlockClass::Hot {
362 continue;
363 }
364
365 let rows: Vec<InterpolatedRow> = match (&block.data, block_b) {
366 (BlockData::Indexed8(items_a), BlockData::Indexed8(items_b)) => items_a
367 .iter()
368 .filter_map(|(id, row_a)| {
369 let (Some(row_a), Some(Some(row_b))) = (row_a, items_b.get(id)) else {
370 return None;
371 };
372
373 Some(InterpolatedRow {
374 id: *id as u32,
375 fields: interpolate_row(&schema.fields, row_a, row_b, alpha),
376 })
377 })
378 .collect(),
379 (BlockData::IndexedNoNull8(items_a), BlockData::IndexedNoNull8(items_b)) => items_a
380 .iter()
381 .filter_map(|(index, fields_a)| {
382 let fields_b = items_b.get(index)?;
383
384 Some(InterpolatedRow {
385 id: *index as u32,
386 fields: interpolate_row(&schema.fields, fields_a, fields_b, alpha),
387 })
388 })
389 .collect(),
390 _ => continue,
392 };
393
394 if !rows.is_empty() {
395 game.blocks.insert(block.key.clone(), rows);
396 }
397 }
398
399 game
400}
401
402fn interpolate_camera(
404 a: Option<&DecodedCamera>,
405 b: Option<&DecodedCamera>,
406 alpha: f32,
407) -> Option<[f32; 2]> {
408 match (a, b) {
409 (Some(a), Some(b)) => Some([lerp(a.x, b.x, alpha), lerp(a.y, b.y, alpha)]),
410 _ => strip_camera(a),
411 }
412}
413
414fn strip_camera(camera: Option<&DecodedCamera>) -> Option<[f32; 2]> {
416 camera.map(|camera| [camera.x, camera.y])
417}
418
419#[cfg(test)]
420mod tests {
421 use indexmap::IndexMap;
422
423 use super::*;
424 use crate::config::test_support::full_snapshot_config;
425
426 use super::super::unpack::DecodedBlock;
427
428 const DELAY: f64 = 100.0;
429
430 fn make() -> Interpolator {
431 Interpolator::new(
432 &InterpolationConfig {
433 delay: DELAY,
434 max_frame_age: 1000.0,
435 },
436 full_snapshot_config(3, 5),
437 )
438 }
439
440 fn tank_row(x: f32, angle: f32) -> Vec<FieldValue> {
441 vec![
442 FieldValue::F32(x),
443 FieldValue::F32(0.0),
444 FieldValue::F32(angle),
445 FieldValue::F32(0.0),
446 FieldValue::F32(0.0),
447 FieldValue::F32(0.0),
448 FieldValue::F32(0.0),
449 FieldValue::U8(3),
450 FieldValue::U8(2),
451 FieldValue::U8(1),
452 ]
453 }
454
455 fn tank_frame(entries: &[(u8, Option<Vec<FieldValue>>)]) -> FrameData {
456 let mut items = IndexMap::new();
457
458 for (id, row) in entries {
459 items.insert(*id, row.clone());
460 }
461
462 FrameData {
463 snapshot: DecodedSnapshot {
464 blocks: vec![DecodedBlock {
465 key: "m1".to_string(),
466 key_id: 1,
467 data: BlockData::Indexed8(items),
468 }],
469 },
470 camera: None,
471 }
472 }
473
474 fn push_zero_offset(i: &mut Interpolator, frame: FrameData, server_time: f64, seq: u32) {
476 i.push(frame, server_time, server_time, seq);
477 }
478
479 fn tanks(game: &InterpolatedGame) -> &[InterpolatedRow] {
480 game.blocks.get("m1").map_or(&[], Vec::as_slice)
481 }
482
483 fn f32_at(row: &InterpolatedRow, i: usize) -> f32 {
484 match row.fields[i] {
485 FieldValue::F32(v) => v,
486 _ => panic!("поле {i} не F32"),
487 }
488 }
489
490 fn u8_at(row: &InterpolatedRow, i: usize) -> u8 {
491 match row.fields[i] {
492 FieldValue::U8(v) => v,
493 _ => panic!("поле {i} не U8"),
494 }
495 }
496
497 #[test]
498 fn empty_buffer_returns_nothing() {
499 let mut i = make();
500 let result = i.sample(0.0);
501
502 assert!(result.frames.is_empty());
503 assert!(result.game.is_none());
504 assert!(result.camera.is_none());
505 }
506
507 #[test]
508 fn render_time_before_first_frame_returns_nothing() {
509 let mut i = make();
510
511 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
512
513 let result = i.sample(1000.0);
515
516 assert!(result.frames.is_empty());
517 assert!(result.game.is_none());
518 }
519
520 #[test]
521 fn positions_lerp_between_frames() {
522 let mut i = make();
523
524 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
525 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(100.0, 0.0)))]), 1100.0, 2);
526
527 let result = i.sample(1150.0);
529 let game = result.game.unwrap();
530 let rows = tanks(&game);
531
532 assert_eq!(rows.len(), 1);
533 assert_eq!(f32_at(&rows[0], 0), 50.0);
534 assert_eq!(rows[0].id, 1);
535 }
536
537 #[test]
538 fn angles_lerp_shortest_path() {
539 use std::f32::consts::PI;
540
541 let mut i = make();
542
543 push_zero_offset(
544 &mut i,
545 tank_frame(&[(1, Some(tank_row(0.0, PI - 0.1)))]),
546 1000.0,
547 1,
548 );
549 push_zero_offset(
550 &mut i,
551 tank_frame(&[(1, Some(tank_row(0.0, -PI + 0.1)))]),
552 1100.0,
553 2,
554 );
555
556 let result = i.sample(1150.0);
557 let game = result.game.unwrap();
558 let angle = f32_at(&tanks(&game)[0], 2);
559
560 assert!((angle.abs() - PI).abs() < 0.01);
562 }
563
564 #[test]
565 fn discrete_fields_come_from_frame_a() {
566 let mut i = make();
567 let mut row_b = tank_row(100.0, 0.0);
568
569 row_b[7] = FieldValue::U8(1); push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
572 push_zero_offset(&mut i, tank_frame(&[(1, Some(row_b))]), 1100.0, 2);
573
574 let result = i.sample(1150.0);
575 let game = result.game.unwrap();
576
577 assert_eq!(u8_at(&tanks(&game)[0], 7), 3);
578 }
579
580 #[test]
581 fn null_and_missing_tanks_are_not_interpolated() {
582 let mut i = make();
583
584 push_zero_offset(
585 &mut i,
586 tank_frame(&[(1, Some(tank_row(0.0, 0.0))), (2, Some(tank_row(5.0, 0.0)))]),
587 1000.0,
588 1,
589 );
590 push_zero_offset(&mut i, tank_frame(&[(1, None)]), 1100.0, 2);
592
593 let result = i.sample(1150.0);
594
595 assert!(tanks(&result.game.unwrap()).is_empty());
596 }
597
598 #[test]
599 fn frames_are_issued_exactly_once() {
600 let mut i = make();
601
602 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
603 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(10.0, 0.0)))]), 1100.0, 2);
604
605 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); }
609
610 #[test]
611 fn hold_on_last_frame_without_extrapolation() {
612 let mut i = make();
613
614 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(50.0, 0.0)))]), 1000.0, 1);
615
616 let result = i.sample(1500.0);
617
618 let game = result.game.unwrap();
620 assert_eq!(f32_at(&tanks(&game)[0], 0), 50.0);
621 }
622
623 #[test]
624 fn out_of_order_insert_and_duplicates() {
625 let mut i = make();
626
627 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
628 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(100.0, 0.0)))]), 1200.0, 3);
629 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(50.0, 0.0)))]), 1100.0, 2);
631 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(999.0, 0.0)))]), 1100.0, 2);
633
634 let result = i.sample(1250.0);
636
637 let game = result.game.unwrap();
638 assert_eq!(f32_at(&tanks(&game)[0], 0), 75.0);
639 assert_eq!(result.frames.len(), 2); }
641
642 #[test]
643 fn late_frame_events_are_issued_immediately() {
644 let mut i = make();
645
646 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
647 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(20.0, 0.0)))]), 1200.0, 3);
648 i.sample(1250.0); push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(10.0, 0.0)))]), 1100.0, 2);
652
653 let result = i.sample(1251.0);
654
655 assert_eq!(result.frames.len(), 1); push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(10.0, 0.0)))]), 1100.0, 2);
659 assert!(i.sample(1252.0).frames.is_empty());
660 }
661
662 #[test]
663 fn camera_lerp_and_strip() {
664 let mut i = make();
665 let frame = |x: f32| FrameData {
666 snapshot: DecodedSnapshot::default(),
667 camera: Some(DecodedCamera {
668 x,
669 y: 0.0,
670 force_reset: true,
671 shake: None,
672 }),
673 };
674
675 push_zero_offset(&mut i, frame(0.0), 1000.0, 1);
676 push_zero_offset(&mut i, frame(100.0), 1100.0, 2);
677
678 let result = i.sample(1150.0);
679
680 assert_eq!(result.camera, Some([50.0, 0.0]));
682 }
683
684 #[test]
685 fn offset_ema_smoothing() {
686 let mut i = make();
687
688 i.push(tank_frame(&[]), 1000.0, 500.0, 1); assert_eq!(i.offset(), Some(500.0));
690
691 i.push(tank_frame(&[]), 1100.0, 500.0, 2); assert_eq!(i.offset(), Some(510.0));
694 }
695
696 #[test]
697 fn old_frames_are_pruned_by_age() {
698 let mut i = make();
699
700 push_zero_offset(&mut i, tank_frame(&[]), 1000.0, 1);
701 push_zero_offset(&mut i, tank_frame(&[]), 1100.0, 2);
702 push_zero_offset(&mut i, tank_frame(&[]), 2500.0, 3);
704
705 let result = i.sample(2500.0); assert_eq!(result.frames.len(), 1);
711 }
712
713 #[test]
714 fn reset_clears_everything() {
715 let mut i = make();
716
717 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
718 i.reset();
719
720 assert!(i.offset().is_none());
721
722 let result = i.sample(2000.0);
723
724 assert!(result.frames.is_empty());
725 assert!(result.game.is_none());
726
727 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(5.0, 0.0)))]), 3000.0, 1);
729 assert_eq!(i.sample(3200.0).frames.len(), 1);
730 }
731}