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 reset(&mut self) {
262 self.frames.clear();
263 self.offset_ema = None;
264 self.last_render_time = None;
265 self.pending_late.clear();
266 self.issued_seqs.clear();
267 }
268
269 fn prune_issued_seqs(&mut self) {
271 let min_seq = self.frames[0].seq.saturating_sub(SEQ_DEDUP_WINDOW);
272
273 self.issued_seqs.retain(|seq| *seq >= min_seq);
274 }
275}
276
277fn interp_field(interp: Interp, a: FieldValue, b: FieldValue, alpha: f32) -> FieldValue {
283 match (interp, a, b) {
284 (Interp::Lerp, FieldValue::F32(a), FieldValue::F32(b)) => FieldValue::F32(lerp(a, b, alpha)),
285 (Interp::LerpAngle, FieldValue::F32(a), FieldValue::F32(b)) => {
286 FieldValue::F32(lerp_angle(a, b, alpha))
287 }
288 _ => a,
289 }
290}
291
292fn interpolate_row(
294 schema_fields: &[crate::config::FieldSchema],
295 fields_a: &[FieldValue],
296 fields_b: &[FieldValue],
297 alpha: f32,
298) -> Vec<FieldValue> {
299 fields_a
300 .iter()
301 .zip(fields_b)
302 .enumerate()
303 .map(|(i, (a, b))| {
304 let interp = schema_fields.get(i).map_or(Interp::Discrete, |f| f.interp);
305
306 interp_field(interp, *a, *b, alpha)
307 })
308 .collect()
309}
310
311fn interpolate_game(
318 a: &DecodedSnapshot,
319 b: &DecodedSnapshot,
320 alpha: f32,
321 cfg: &SnapshotConfig,
322) -> InterpolatedGame {
323 let mut game = InterpolatedGame::default();
324
325 for block in &a.blocks {
326 let Some(block_b) = b.block_by_key(&block.key) else {
327 continue;
328 };
329
330 let Some(schema) = cfg.keys.get(&block.key) else {
331 continue;
332 };
333
334 if schema.class != BlockClass::Hot {
335 continue;
336 }
337
338 let rows: Vec<InterpolatedRow> = match (&block.data, block_b) {
339 (BlockData::Indexed8(items_a), BlockData::Indexed8(items_b)) => items_a
340 .iter()
341 .filter_map(|(id, row_a)| {
342 let (Some(row_a), Some(Some(row_b))) = (row_a, items_b.get(id)) else {
343 return None;
344 };
345
346 Some(InterpolatedRow {
347 id: *id as u32,
348 fields: interpolate_row(&schema.fields, row_a, row_b, alpha),
349 })
350 })
351 .collect(),
352 (BlockData::IndexedNoNull8(items_a), BlockData::IndexedNoNull8(items_b)) => items_a
353 .iter()
354 .filter_map(|(index, fields_a)| {
355 let fields_b = items_b.get(index)?;
356
357 Some(InterpolatedRow {
358 id: *index as u32,
359 fields: interpolate_row(&schema.fields, fields_a, fields_b, alpha),
360 })
361 })
362 .collect(),
363 _ => continue,
365 };
366
367 if !rows.is_empty() {
368 game.blocks.insert(block.key.clone(), rows);
369 }
370 }
371
372 game
373}
374
375fn interpolate_camera(
377 a: Option<&DecodedCamera>,
378 b: Option<&DecodedCamera>,
379 alpha: f32,
380) -> Option<[f32; 2]> {
381 match (a, b) {
382 (Some(a), Some(b)) => Some([lerp(a.x, b.x, alpha), lerp(a.y, b.y, alpha)]),
383 _ => strip_camera(a),
384 }
385}
386
387fn strip_camera(camera: Option<&DecodedCamera>) -> Option<[f32; 2]> {
389 camera.map(|camera| [camera.x, camera.y])
390}
391
392#[cfg(test)]
393mod tests {
394 use indexmap::IndexMap;
395
396 use super::*;
397 use crate::config::test_support::full_snapshot_config;
398
399 use super::super::unpack::DecodedBlock;
400
401 const DELAY: f64 = 100.0;
402
403 fn make() -> Interpolator {
404 Interpolator::new(
405 &InterpolationConfig {
406 delay: DELAY,
407 max_frame_age: 1000.0,
408 },
409 full_snapshot_config(3, 5),
410 )
411 }
412
413 fn tank_row(x: f32, angle: f32) -> Vec<FieldValue> {
414 vec![
415 FieldValue::F32(x),
416 FieldValue::F32(0.0),
417 FieldValue::F32(angle),
418 FieldValue::F32(0.0),
419 FieldValue::F32(0.0),
420 FieldValue::F32(0.0),
421 FieldValue::F32(0.0),
422 FieldValue::U8(3),
423 FieldValue::U8(2),
424 FieldValue::U8(1),
425 ]
426 }
427
428 fn tank_frame(entries: &[(u8, Option<Vec<FieldValue>>)]) -> FrameData {
429 let mut items = IndexMap::new();
430
431 for (id, row) in entries {
432 items.insert(*id, row.clone());
433 }
434
435 FrameData {
436 snapshot: DecodedSnapshot {
437 blocks: vec![DecodedBlock {
438 key: "m1".to_string(),
439 key_id: 1,
440 data: BlockData::Indexed8(items),
441 }],
442 },
443 camera: None,
444 }
445 }
446
447 fn push_zero_offset(i: &mut Interpolator, frame: FrameData, server_time: f64, seq: u32) {
449 i.push(frame, server_time, server_time, seq);
450 }
451
452 fn tanks(game: &InterpolatedGame) -> &[InterpolatedRow] {
453 game.blocks.get("m1").map_or(&[], Vec::as_slice)
454 }
455
456 fn f32_at(row: &InterpolatedRow, i: usize) -> f32 {
457 match row.fields[i] {
458 FieldValue::F32(v) => v,
459 _ => panic!("поле {i} не F32"),
460 }
461 }
462
463 fn u8_at(row: &InterpolatedRow, i: usize) -> u8 {
464 match row.fields[i] {
465 FieldValue::U8(v) => v,
466 _ => panic!("поле {i} не U8"),
467 }
468 }
469
470 #[test]
471 fn empty_buffer_returns_nothing() {
472 let mut i = make();
473 let result = i.sample(0.0);
474
475 assert!(result.frames.is_empty());
476 assert!(result.game.is_none());
477 assert!(result.camera.is_none());
478 }
479
480 #[test]
481 fn render_time_before_first_frame_returns_nothing() {
482 let mut i = make();
483
484 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
485
486 let result = i.sample(1000.0);
488
489 assert!(result.frames.is_empty());
490 assert!(result.game.is_none());
491 }
492
493 #[test]
494 fn positions_lerp_between_frames() {
495 let mut i = make();
496
497 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
498 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(100.0, 0.0)))]), 1100.0, 2);
499
500 let result = i.sample(1150.0);
502 let game = result.game.unwrap();
503 let rows = tanks(&game);
504
505 assert_eq!(rows.len(), 1);
506 assert_eq!(f32_at(&rows[0], 0), 50.0);
507 assert_eq!(rows[0].id, 1);
508 }
509
510 #[test]
511 fn angles_lerp_shortest_path() {
512 use std::f32::consts::PI;
513
514 let mut i = make();
515
516 push_zero_offset(
517 &mut i,
518 tank_frame(&[(1, Some(tank_row(0.0, PI - 0.1)))]),
519 1000.0,
520 1,
521 );
522 push_zero_offset(
523 &mut i,
524 tank_frame(&[(1, Some(tank_row(0.0, -PI + 0.1)))]),
525 1100.0,
526 2,
527 );
528
529 let result = i.sample(1150.0);
530 let game = result.game.unwrap();
531 let angle = f32_at(&tanks(&game)[0], 2);
532
533 assert!((angle.abs() - PI).abs() < 0.01);
535 }
536
537 #[test]
538 fn discrete_fields_come_from_frame_a() {
539 let mut i = make();
540 let mut row_b = tank_row(100.0, 0.0);
541
542 row_b[7] = FieldValue::U8(1); push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
545 push_zero_offset(&mut i, tank_frame(&[(1, Some(row_b))]), 1100.0, 2);
546
547 let result = i.sample(1150.0);
548 let game = result.game.unwrap();
549
550 assert_eq!(u8_at(&tanks(&game)[0], 7), 3);
551 }
552
553 #[test]
554 fn null_and_missing_tanks_are_not_interpolated() {
555 let mut i = make();
556
557 push_zero_offset(
558 &mut i,
559 tank_frame(&[(1, Some(tank_row(0.0, 0.0))), (2, Some(tank_row(5.0, 0.0)))]),
560 1000.0,
561 1,
562 );
563 push_zero_offset(&mut i, tank_frame(&[(1, None)]), 1100.0, 2);
565
566 let result = i.sample(1150.0);
567
568 assert!(tanks(&result.game.unwrap()).is_empty());
569 }
570
571 #[test]
572 fn frames_are_issued_exactly_once() {
573 let mut i = make();
574
575 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
576 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(10.0, 0.0)))]), 1100.0, 2);
577
578 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); }
582
583 #[test]
584 fn hold_on_last_frame_without_extrapolation() {
585 let mut i = make();
586
587 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(50.0, 0.0)))]), 1000.0, 1);
588
589 let result = i.sample(1500.0);
590
591 let game = result.game.unwrap();
593 assert_eq!(f32_at(&tanks(&game)[0], 0), 50.0);
594 }
595
596 #[test]
597 fn out_of_order_insert_and_duplicates() {
598 let mut i = make();
599
600 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
601 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(100.0, 0.0)))]), 1200.0, 3);
602 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(50.0, 0.0)))]), 1100.0, 2);
604 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(999.0, 0.0)))]), 1100.0, 2);
606
607 let result = i.sample(1250.0);
609
610 let game = result.game.unwrap();
611 assert_eq!(f32_at(&tanks(&game)[0], 0), 75.0);
612 assert_eq!(result.frames.len(), 2); }
614
615 #[test]
616 fn late_frame_events_are_issued_immediately() {
617 let mut i = make();
618
619 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
620 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(20.0, 0.0)))]), 1200.0, 3);
621 i.sample(1250.0); push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(10.0, 0.0)))]), 1100.0, 2);
625
626 let result = i.sample(1251.0);
627
628 assert_eq!(result.frames.len(), 1); push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(10.0, 0.0)))]), 1100.0, 2);
632 assert!(i.sample(1252.0).frames.is_empty());
633 }
634
635 #[test]
636 fn camera_lerp_and_strip() {
637 let mut i = make();
638 let frame = |x: f32| FrameData {
639 snapshot: DecodedSnapshot::default(),
640 camera: Some(DecodedCamera {
641 x,
642 y: 0.0,
643 force_reset: true,
644 shake: None,
645 }),
646 };
647
648 push_zero_offset(&mut i, frame(0.0), 1000.0, 1);
649 push_zero_offset(&mut i, frame(100.0), 1100.0, 2);
650
651 let result = i.sample(1150.0);
652
653 assert_eq!(result.camera, Some([50.0, 0.0]));
655 }
656
657 #[test]
658 fn offset_ema_smoothing() {
659 let mut i = make();
660
661 i.push(tank_frame(&[]), 1000.0, 500.0, 1); assert_eq!(i.offset(), Some(500.0));
663
664 i.push(tank_frame(&[]), 1100.0, 500.0, 2); assert_eq!(i.offset(), Some(510.0));
667 }
668
669 #[test]
670 fn old_frames_are_pruned_by_age() {
671 let mut i = make();
672
673 push_zero_offset(&mut i, tank_frame(&[]), 1000.0, 1);
674 push_zero_offset(&mut i, tank_frame(&[]), 1100.0, 2);
675 push_zero_offset(&mut i, tank_frame(&[]), 2500.0, 3);
677
678 let result = i.sample(2500.0); assert_eq!(result.frames.len(), 1);
684 }
685
686 #[test]
687 fn reset_clears_everything() {
688 let mut i = make();
689
690 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(0.0, 0.0)))]), 1000.0, 1);
691 i.reset();
692
693 assert!(i.offset().is_none());
694
695 let result = i.sample(2000.0);
696
697 assert!(result.frames.is_empty());
698 assert!(result.game.is_none());
699
700 push_zero_offset(&mut i, tank_frame(&[(1, Some(tank_row(5.0, 0.0)))]), 3000.0, 1);
702 assert_eq!(i.sample(3200.0).frames.len(), 1);
703 }
704}