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(
317 schema_fields: &[crate::config::FieldSchema],
318 fields_a: &[FieldValue],
319 fields_b: &[FieldValue],
320 alpha: f32,
321) -> Vec<FieldValue> {
322 fields_a
323 .iter()
324 .zip(fields_b)
325 .enumerate()
326 .map(|(i, (a, b))| {
327 let interp = schema_fields.get(i).map_or(Interp::Discrete, |f| f.interp);
328
329 interp_field(interp, *a, *b, alpha)
330 })
331 .collect()
332}
333
334fn interpolate_game(
341 a: &DecodedSnapshot,
342 b: &DecodedSnapshot,
343 alpha: f32,
344 cfg: &SnapshotConfig,
345) -> InterpolatedGame {
346 let mut game = InterpolatedGame::default();
347
348 for block in &a.blocks {
349 let Some(block_b) = b.block_by_key(&block.key) else {
350 continue;
351 };
352
353 let Some(schema) = cfg.keys.get(&block.key) else {
354 continue;
355 };
356
357 if schema.class != BlockClass::Hot {
358 continue;
359 }
360
361 let rows: Vec<InterpolatedRow> = match (&block.data, block_b) {
362 (BlockData::Indexed8(items_a), BlockData::Indexed8(items_b)) => items_a
363 .iter()
364 .filter_map(|(id, row_a)| {
365 let (Some(row_a), Some(Some(row_b))) = (row_a, items_b.get(id)) else {
366 return None;
367 };
368
369 Some(InterpolatedRow {
370 id: *id as u32,
371 fields: interpolate_row(&schema.fields, row_a, row_b, alpha),
372 })
373 })
374 .collect(),
375 (BlockData::IndexedNoNull8(items_a), BlockData::IndexedNoNull8(items_b)) => items_a
376 .iter()
377 .filter_map(|(index, fields_a)| {
378 let fields_b = items_b.get(index)?;
379
380 Some(InterpolatedRow {
381 id: *index as u32,
382 fields: interpolate_row(&schema.fields, fields_a, fields_b, alpha),
383 })
384 })
385 .collect(),
386 _ => continue,
388 };
389
390 if !rows.is_empty() {
391 game.blocks.insert(block.key.clone(), rows);
392 }
393 }
394
395 game
396}
397
398fn interpolate_camera(
400 a: Option<&DecodedCamera>,
401 b: Option<&DecodedCamera>,
402 alpha: f32,
403) -> Option<[f32; 2]> {
404 match (a, b) {
405 (Some(a), Some(b)) => Some([lerp(a.x, b.x, alpha), lerp(a.y, b.y, alpha)]),
406 _ => strip_camera(a),
407 }
408}
409
410fn strip_camera(camera: Option<&DecodedCamera>) -> Option<[f32; 2]> {
412 camera.map(|camera| [camera.x, camera.y])
413}
414
415#[cfg(test)]
416mod tests {
417 use indexmap::IndexMap;
418
419 use super::*;
420 use crate::config::test_support::full_snapshot_config;
421
422 use super::super::unpack::DecodedBlock;
423
424 const DELAY: f64 = 100.0;
425
426 fn make() -> Interpolator {
427 Interpolator::new(
428 &InterpolationConfig {
429 delay: DELAY,
430 max_frame_age: 1000.0,
431 },
432 full_snapshot_config(3, 5),
433 )
434 }
435
436 fn tank_row(x: f32, angle: f32) -> Vec<FieldValue> {
437 vec![
438 FieldValue::F32(x),
439 FieldValue::F32(0.0),
440 FieldValue::F32(angle),
441 FieldValue::F32(0.0),
442 FieldValue::F32(0.0),
443 FieldValue::F32(0.0),
444 FieldValue::F32(0.0),
445 FieldValue::U8(3),
446 FieldValue::U8(2),
447 FieldValue::U8(1),
448 ]
449 }
450
451 fn tank_frame(entries: &[(u8, Option<Vec<FieldValue>>)]) -> FrameData {
452 let mut items = IndexMap::new();
453
454 for (id, row) in entries {
455 items.insert(*id, row.clone());
456 }
457
458 FrameData {
459 snapshot: DecodedSnapshot {
460 blocks: vec![DecodedBlock {
461 key: "m1".to_string(),
462 key_id: 1,
463 data: BlockData::Indexed8(items),
464 }],
465 },
466 camera: None,
467 }
468 }
469
470 fn push_zero_offset(i: &mut Interpolator, frame: FrameData, server_time: f64, seq: u32) {
472 i.push(frame, server_time, server_time, seq);
473 }
474
475 fn tanks(game: &InterpolatedGame) -> &[InterpolatedRow] {
476 game.blocks.get("m1").map_or(&[], Vec::as_slice)
477 }
478
479 fn f32_at(row: &InterpolatedRow, i: usize) -> f32 {
480 match row.fields[i] {
481 FieldValue::F32(v) => v,
482 _ => panic!("поле {i} не F32"),
483 }
484 }
485
486 fn u8_at(row: &InterpolatedRow, i: usize) -> u8 {
487 match row.fields[i] {
488 FieldValue::U8(v) => v,
489 _ => panic!("поле {i} не U8"),
490 }
491 }
492
493 #[test]
494 fn empty_buffer_returns_nothing() {
495 let mut i = make();
496 let result = i.sample(0.0);
497
498 assert!(result.frames.is_empty());
499 assert!(result.game.is_none());
500 assert!(result.camera.is_none());
501 }
502
503 #[test]
504 fn render_time_before_first_frame_returns_nothing() {
505 let mut i = make();
506
507 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
508
509 let result = i.sample(1000.0);
511
512 assert!(result.frames.is_empty());
513 assert!(result.game.is_none());
514 }
515
516 #[test]
517 fn positions_lerp_between_frames() {
518 let mut i = make();
519
520 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
521 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(100.0, 0.0)))]), 1100.0, 2);
522
523 let result = i.sample(1150.0);
525 let game = result.game.unwrap();
526 let rows = tanks(&game);
527
528 assert_eq!(rows.len(), 1);
529 assert_eq!(f32_at(&rows[0], 0), 50.0);
530 assert_eq!(rows[0].id, 1);
531 }
532
533 #[test]
534 fn angles_lerp_shortest_path() {
535 use std::f32::consts::PI;
536
537 let mut i = make();
538
539 push_zero_offset(
540 &mut i,
541 tank_frame(&[(1, Some(tank_row(0.0, PI - 0.1)))]),
542 1000.0,
543 1,
544 );
545 push_zero_offset(
546 &mut i,
547 tank_frame(&[(1, Some(tank_row(0.0, -PI + 0.1)))]),
548 1100.0,
549 2,
550 );
551
552 let result = i.sample(1150.0);
553 let game = result.game.unwrap();
554 let angle = f32_at(&tanks(&game)[0], 2);
555
556 assert!((angle.abs() - PI).abs() < 0.01);
558 }
559
560 #[test]
561 fn discrete_fields_come_from_frame_a() {
562 let mut i = make();
563 let mut row_b = tank_row(100.0, 0.0);
564
565 row_b[7] = FieldValue::U8(1); push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
568 push_zero_offset(&mut i, tank_frame(&[(1, Some(row_b))]), 1100.0, 2);
569
570 let result = i.sample(1150.0);
571 let game = result.game.unwrap();
572
573 assert_eq!(u8_at(&tanks(&game)[0], 7), 3);
574 }
575
576 #[test]
577 fn null_and_missing_tanks_are_not_interpolated() {
578 let mut i = make();
579
580 push_zero_offset(
581 &mut i,
582 tank_frame(&[(1, Some(tank_row(0.0, 0.0))), (2, Some(tank_row(5.0, 0.0)))]),
583 1000.0,
584 1,
585 );
586 push_zero_offset(&mut i, tank_frame(&[(1, None)]), 1100.0, 2);
588
589 let result = i.sample(1150.0);
590
591 assert!(tanks(&result.game.unwrap()).is_empty());
592 }
593
594 #[test]
595 fn frames_are_issued_exactly_once() {
596 let mut i = make();
597
598 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
599 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(10.0, 0.0)))]), 1100.0, 2);
600
601 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); }
605
606 #[test]
607 fn hold_on_last_frame_without_extrapolation() {
608 let mut i = make();
609
610 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(50.0, 0.0)))]), 1000.0, 1);
611
612 let result = i.sample(1500.0);
613
614 let game = result.game.unwrap();
616 assert_eq!(f32_at(&tanks(&game)[0], 0), 50.0);
617 }
618
619 #[test]
620 fn out_of_order_insert_and_duplicates() {
621 let mut i = make();
622
623 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
624 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(100.0, 0.0)))]), 1200.0, 3);
625 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(50.0, 0.0)))]), 1100.0, 2);
627 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(999.0, 0.0)))]), 1100.0, 2);
629
630 let result = i.sample(1250.0);
632
633 let game = result.game.unwrap();
634 assert_eq!(f32_at(&tanks(&game)[0], 0), 75.0);
635 assert_eq!(result.frames.len(), 2); }
637
638 #[test]
639 fn late_frame_events_are_issued_immediately() {
640 let mut i = make();
641
642 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
643 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(20.0, 0.0)))]), 1200.0, 3);
644 i.sample(1250.0); push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(10.0, 0.0)))]), 1100.0, 2);
648
649 let result = i.sample(1251.0);
650
651 assert_eq!(result.frames.len(), 1); push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(10.0, 0.0)))]), 1100.0, 2);
655 assert!(i.sample(1252.0).frames.is_empty());
656 }
657
658 #[test]
659 fn camera_lerp_and_strip() {
660 let mut i = make();
661 let frame = |x: f32| FrameData {
662 snapshot: DecodedSnapshot::default(),
663 camera: Some(DecodedCamera {
664 x,
665 y: 0.0,
666 force_reset: true,
667 shake: None,
668 }),
669 };
670
671 push_zero_offset(&mut i, frame(0.0), 1000.0, 1);
672 push_zero_offset(&mut i, frame(100.0), 1100.0, 2);
673
674 let result = i.sample(1150.0);
675
676 assert_eq!(result.camera, Some([50.0, 0.0]));
678 }
679
680 #[test]
681 fn offset_ema_smoothing() {
682 let mut i = make();
683
684 i.push(tank_frame(&[]), 1000.0, 500.0, 1); assert_eq!(i.offset(), Some(500.0));
686
687 i.push(tank_frame(&[]), 1100.0, 500.0, 2); assert_eq!(i.offset(), Some(510.0));
690 }
691
692 #[test]
693 fn old_frames_are_pruned_by_age() {
694 let mut i = make();
695
696 push_zero_offset(&mut i, tank_frame(&[]), 1000.0, 1);
697 push_zero_offset(&mut i, tank_frame(&[]), 1100.0, 2);
698 push_zero_offset(&mut i, tank_frame(&[]), 2500.0, 3);
700
701 let result = i.sample(2500.0); assert_eq!(result.frames.len(), 1);
707 }
708
709 #[test]
710 fn reset_clears_everything() {
711 let mut i = make();
712
713 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
714 i.reset();
715
716 assert!(i.offset().is_none());
717
718 let result = i.sample(2000.0);
719
720 assert!(result.frames.is_empty());
721 assert!(result.game.is_none());
722
723 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(5.0, 0.0)))]), 3000.0, 1);
725 assert_eq!(i.sample(3200.0).frames.len(), 1);
726 }
727}