1use serde_json::{Map, Value, json};
23
24use super::divergence::{DivergenceTracker, Observation, Source};
25use super::interpolator::{FrameData, InterpolatedGame, Interpolator};
26use super::unpack::{self, DecodedSnapshot, UnpackError};
27use crate::config::{BlockKind, EngineClientConfig, FieldValue, PLAYER_STATE_LEN, SnapshotConfig};
28
29pub struct RenderOverlay {
32 pub camera: [f32; 2],
33 pub tail: Vec<f32>,
34}
35
36pub struct PredictedRow {
43 pub key_id: u8,
44 pub id: u32,
45 pub fields: Vec<f32>,
46}
47
48pub trait GameClientDef: Sized {
53 type Config: serde::de::DeserializeOwned;
54
55 fn new(cfg: &Self::Config, engine_cfg: &EngineClientConfig) -> Self;
56
57 fn on_server_state(
62 &mut self,
63 state: [f32; PLAYER_STATE_LEN],
64 centering: bool,
65 server_time: f64,
66 offset: f64,
67 local_now: f64,
68 );
69
70 fn update(&mut self, local_now: f64);
72
73 fn begin_reconcile(&mut self, _snapshot: &DecodedSnapshot) {}
78
79 fn finish_reconcile(&mut self) {}
83
84 fn render_rows(&self) -> Vec<PredictedRow> {
87 Vec::new()
88 }
89
90 fn track_frame(&mut self, my_game_id: Option<u32>, frame: &FrameData);
94
95 fn filter_frame_game(&mut self, game: &mut Map<String, Value>, my_game_id: Option<u32>, local_now: f64);
98
99 fn update_world(&mut self, snapshot: &DecodedSnapshot);
102
103 fn update_world_interpolated(&mut self, game: &InterpolatedGame);
105
106 fn render_overlay(&self, my_game_id: Option<u32>) -> Option<RenderOverlay>;
110
111 fn predicted_state(&self) -> Option<[f32; PLAYER_STATE_LEN]> {
118 None
119 }
120
121 fn replayed_inputs(&self) -> Option<(f64, f64, usize)> {
126 None
127 }
128
129 fn apply_input(&mut self, action: &str, key_name: &str, local_now: f64);
130
131 fn apply_aim(&mut self, _x: f32, _y: f32, _flags: u32, _local_now: f64) {}
135 fn set_model(&mut self, model_name: &str);
136 fn set_active(&mut self, active: bool);
137 fn set_map(&mut self, map_json: &str) -> Result<(), String>;
138 fn sync_panel(&mut self, items: &[String]);
139 fn reset(&mut self);
140
141 fn cycle_item(&mut self, back: bool);
144
145 fn try_action(&mut self, my_game_id: Option<u32>, local_now: f64) -> Option<String>;
148}
149
150fn field_as_f32(value: FieldValue) -> f32 {
152 match value {
153 FieldValue::F32(v) => v,
154 FieldValue::U8(v) => v as f32,
155 FieldValue::U16(v) => v as f32,
156 FieldValue::U32(v) => v as f32,
157 }
158}
159
160fn blocks_of_kind<'a>(
163 snapshot_cfg: &'a SnapshotConfig,
164 game: &'a InterpolatedGame,
165 kind: BlockKind,
166) -> impl Iterator<Item = (u8, &'a Vec<super::interpolator::InterpolatedRow>)> {
167 game.blocks.iter().filter_map(move |(key, rows)| {
168 let schema = snapshot_cfg.keys.get(key)?;
169
170 (schema.kind == kind).then_some((schema.id, rows))
171 })
172}
173
174pub struct ClientState<G: GameClientDef> {
179 cfg: EngineClientConfig,
180 interpolator: Interpolator,
181 game: G,
182
183 my_game_id: Option<u32>,
185
186 frames_out: Vec<Value>,
188
189 hot: Vec<f32>,
191
192 divergence: Option<DivergenceTracker>,
195}
196
197impl<G: GameClientDef> ClientState<G> {
198 pub fn new(cfg: EngineClientConfig, game_cfg: &G::Config) -> Self {
199 let interpolator = Interpolator::new(&cfg.interpolation, cfg.snapshot.clone());
200 let game = G::new(game_cfg, &cfg);
201 let divergence = cfg.divergence.clone().map(DivergenceTracker::new);
202
203 Self {
204 cfg,
205 interpolator,
206 game,
207 my_game_id: None,
208 frames_out: Vec::new(),
209 hot: Vec::new(),
210 divergence,
211 }
212 }
213
214 pub fn push_frame(&mut self, data: &[u8], local_now: f64) -> bool {
218 let frame = match unpack::unpack_frame(data, &self.cfg.snapshot) {
219 Ok(frame) => frame,
220 Err(UnpackError::WrongVersion | UnpackError::Truncated) => return false,
221 };
222
223 if frame.port != self.cfg.snapshot.port {
224 return false;
225 }
226
227 if frame.player.is_some() {
231 self.game.begin_reconcile(&frame.snapshot);
232 }
233
234 self.interpolator.push(
235 FrameData {
236 snapshot: frame.snapshot,
237 camera: frame.camera,
238 },
239 frame.server_time,
240 local_now,
241 frame.seq,
242 );
243
244 if let Some(player) = frame.player {
245 self.my_game_id = Some(player.game_id as u32);
246
247 let offset = self.interpolator.offset().unwrap_or(0.0);
249
250 self.observe_divergence(&player, frame.server_time, offset, local_now);
251
252 self.game.on_server_state(
253 player.state,
254 player.centering,
255 frame.server_time,
256 offset,
257 local_now,
258 );
259
260 self.game.finish_reconcile();
262 }
263
264 true
265 }
266
267 pub fn game(&self) -> &G {
270 &self.game
271 }
272
273 pub fn my_game_id(&self) -> Option<u32> {
274 self.my_game_id
275 }
276
277 pub fn offset(&self) -> Option<f64> {
278 self.interpolator.offset()
279 }
280
281 pub fn sample(&mut self, local_now: f64) -> usize {
285 let result = self.interpolator.sample(local_now);
286
287 for frame in result.frames {
289 self.game.track_frame(self.my_game_id, &frame);
290
291 let mut game = unpack::snapshot_to_json(&frame.snapshot);
292
293 self.game
294 .filter_frame_game(&mut game, self.my_game_id, local_now);
295
296 self.frames_out.push(json!({
297 "game": game,
298 "camera": unpack::camera_to_json(frame.camera.as_ref()),
299 }));
300
301 self.game.update_world(&frame.snapshot);
302 }
303
304 if let Some(game) = &result.game {
305 self.game.update_world_interpolated(game);
306 }
307
308 self.game.update(local_now);
309
310 let overlay = self.game.render_overlay(self.my_game_id);
311 let rows = self.game.render_rows();
312
313 self.write_hot(result.game.as_ref(), result.camera, overlay.as_ref(), &rows);
314 self.hot.len()
315 }
316
317 pub fn hot(&self) -> &[f32] {
318 &self.hot
319 }
320
321 pub fn take_frames(&mut self) -> String {
323 let frames = std::mem::take(&mut self.frames_out);
324
325 serde_json::to_string(&frames).unwrap_or_else(|_| "[]".to_string())
326 }
327
328 pub fn apply_input(&mut self, action: &str, key_name: &str, local_now: f64) {
329 self.game.apply_input(action, key_name, local_now);
330 }
331
332 pub fn apply_aim(&mut self, x: f32, y: f32, flags: u32, local_now: f64) {
333 self.game.apply_aim(x, y, flags, local_now);
334 }
335
336 pub fn try_action(&mut self, local_now: f64) -> Option<String> {
337 self.game.try_action(self.my_game_id, local_now)
338 }
339
340 pub fn cycle_item(&mut self, back: bool) {
341 self.game.cycle_item(back);
342 }
343
344 pub fn set_model(&mut self, model_name: &str) {
345 self.game.set_model(model_name);
346 }
347
348 pub fn set_active(&mut self, active: bool) {
350 self.game.set_active(active);
351 }
352
353 pub fn set_map(&mut self, map_json: &str) -> Result<(), String> {
355 self.interpolator.reset();
356 self.frames_out.clear();
357 self.game.set_map(map_json)
358 }
359
360 pub fn sync_panel(&mut self, panel_json: &str) {
361 let Ok(Value::Array(items)) = serde_json::from_str(panel_json) else {
362 return;
363 };
364
365 let items: Vec<String> = items
366 .iter()
367 .map(|item| match item {
368 Value::String(s) => s.clone(),
369 other => other.to_string(),
370 })
371 .collect();
372
373 self.game.sync_panel(&items);
374 }
375
376 pub fn reset(&mut self) {
378 self.interpolator.reset();
379 self.game.reset();
380 self.frames_out.clear();
381 self.my_game_id = None;
385 }
386
387 pub fn resync(&mut self) {
393 self.interpolator.reset();
394 self.frames_out.clear();
395 }
396
397 pub fn debug_json(&self) -> String {
401 json!({
402 "myGameId": self.my_game_id,
403 "offset": self.interpolator.offset(),
404 "interpolator": self.interpolator.debug_json(),
405 "hotLen": self.hot.len(),
406 "framesOut": self.frames_out.len(),
407 })
408 .to_string()
409 }
410
411 pub fn take_divergence(&mut self) -> String {
415 match &mut self.divergence {
416 Some(tracker) => tracker.take_json(),
417 None => "null".to_string(),
418 }
419 }
420
421 fn observe_divergence(
424 &mut self,
425 player: &unpack::DecodedPlayer,
426 server_time: f64,
427 offset: f64,
428 local_now: f64,
429 ) {
430 let Some(tracker) = &mut self.divergence else {
431 return;
432 };
433
434 let (source, predicted) = match self.game.predicted_state() {
436 Some(state) => (Source::State, state.to_vec()),
437 None => match self.game.render_overlay(self.my_game_id) {
438 Some(overlay) => (Source::Camera, overlay.camera.to_vec()),
439 None => return,
440 },
441 };
442
443 tracker.observe(Observation {
444 source,
445 predicted: &predicted,
446 authoritative: &player.state,
447 server_time,
448 local_now,
449 offset,
450 input_seq: player.input_seq,
451 replayed: self.game.replayed_inputs(),
452 });
453 }
454
455 pub fn decode_frame(&self, data: &[u8]) -> String {
457 match unpack::unpack_frame(data, &self.cfg.snapshot) {
458 Ok(frame) => unpack::frame_to_json(&frame).to_string(),
459 Err(_) => "null".to_string(),
460 }
461 }
462
463 fn write_hot(
471 &mut self,
472 game: Option<&InterpolatedGame>,
473 camera: Option<[f32; 2]>,
474 overlay: Option<&RenderOverlay>,
475 rows: &[PredictedRow],
476 ) {
477 self.hot.clear();
478
479 let mut flags = 0u32;
480
481 if game.is_some() {
482 flags |= super::HOT_HAS_GAME;
483 }
484
485 if !self.frames_out.is_empty() {
486 flags |= super::HOT_HAS_FRAMES;
487 }
488
489 if overlay.is_some() {
490 flags |= super::HOT_HAS_PREDICTED;
491 }
492
493 let camera = overlay.map(|o| o.camera).or(camera);
495
496 if camera.is_some() {
497 flags |= super::HOT_HAS_CAMERA;
498 }
499
500 self.hot.push(flags as f32);
501
502 let camera = camera.unwrap_or([0.0, 0.0]);
503
504 self.hot.push(camera[0]);
505 self.hot.push(camera[1]);
506
507 let empty = InterpolatedGame::default();
508 let game = game.unwrap_or(&empty);
509
510 let tank_count: usize = blocks_of_kind(&self.cfg.snapshot, game, BlockKind::Indexed8)
515 .map(|(_, rows)| rows.len())
516 .sum();
517
518 self.hot.push(tank_count as f32);
519
520 for (key_id, rows) in blocks_of_kind(&self.cfg.snapshot, game, BlockKind::Indexed8) {
521 for row in rows {
522 self.hot.push(key_id as f32);
523 self.hot.push(row.id as f32);
524
525 for field in &row.fields {
526 self.hot.push(field_as_f32(*field));
527 }
528 }
529 }
530
531 let dynamic_count: usize =
532 blocks_of_kind(&self.cfg.snapshot, game, BlockKind::IndexedNoNull8)
533 .map(|(_, rows)| rows.len())
534 .sum();
535
536 self.hot.push(dynamic_count as f32);
537
538 for (key_id, rows) in blocks_of_kind(&self.cfg.snapshot, game, BlockKind::IndexedNoNull8) {
539 for row in rows {
540 self.hot.push(key_id as f32);
541 self.hot.push(row.id as f32);
542
543 for field in &row.fields {
544 self.hot.push(field_as_f32(*field));
545 }
546 }
547 }
548
549 if let Some(overlay) = overlay {
550 self.hot.extend_from_slice(&overlay.tail);
551 }
552
553 for row in rows {
554 let Some(width) = self
558 .cfg
559 .snapshot
560 .keys
561 .values()
562 .find(|schema| schema.id == row.key_id)
563 .map(|schema| schema.fields.len())
564 else {
565 continue;
566 };
567
568 self.hot.push(row.key_id as f32);
569 self.hot.push(row.id as f32);
570
571 for index in 0..width {
572 self.hot.push(row.fields.get(index).copied().unwrap_or(0.0));
573 }
574 }
575 }
576}
577
578#[cfg(test)]
584mod fixture {
585 use super::*;
586 use serde::Deserialize;
587
588 #[derive(Deserialize)]
589 pub struct TestConfig {}
590
591 pub struct TestClient {
592 x: f32,
593 y: f32,
594 vx: f32,
595 vy: f32,
596 active: bool,
597 alive: bool,
598 last_update: Option<f64>,
599 report_state: bool,
602 predicted_rows: bool,
604 pub reconcile_log: Vec<&'static str>,
606 }
607
608 impl GameClientDef for TestClient {
609 type Config = TestConfig;
610
611 fn new(_cfg: &Self::Config, _engine_cfg: &EngineClientConfig) -> Self {
612 Self {
613 x: 0.0,
614 y: 0.0,
615 vx: 0.0,
616 vy: 0.0,
617 active: false,
618 alive: true,
619 last_update: None,
620 report_state: false,
621 predicted_rows: false,
622 reconcile_log: Vec::new(),
623 }
624 }
625
626 fn on_server_state(
627 &mut self,
628 state: [f32; PLAYER_STATE_LEN],
629 _centering: bool,
630 _server_time: f64,
631 _offset: f64,
632 _local_now: f64,
633 ) {
634 self.x = state[0];
635 self.y = state[1];
636 self.vx = state[3];
637 self.vy = state[4];
638 self.reconcile_log.push("state");
639 }
640
641 fn begin_reconcile(&mut self, _snapshot: &DecodedSnapshot) {
642 self.reconcile_log.push("begin");
643 }
644
645 fn finish_reconcile(&mut self) {
646 self.reconcile_log.push("finish");
647 }
648
649 fn update(&mut self, local_now: f64) {
650 let dt = self
651 .last_update
652 .map(|last| (local_now - last) / 1000.0)
653 .unwrap_or(0.0) as f32;
654
655 self.x += self.vx * dt;
656 self.y += self.vy * dt;
657 self.last_update = Some(local_now);
658 }
659
660 fn track_frame(&mut self, _my_game_id: Option<u32>, _frame: &FrameData) {}
661
662 fn filter_frame_game(
663 &mut self,
664 _game: &mut Map<String, Value>,
665 _my_game_id: Option<u32>,
666 _local_now: f64,
667 ) {
668 }
669
670 fn update_world(&mut self, _snapshot: &DecodedSnapshot) {}
671
672 fn update_world_interpolated(&mut self, _game: &InterpolatedGame) {}
673
674 fn render_overlay(&self, my_game_id: Option<u32>) -> Option<RenderOverlay> {
675 let game_id = my_game_id?;
676
677 (self.active && self.alive).then(|| RenderOverlay {
678 camera: [self.x, self.y],
679 tail: vec![0.0, game_id as f32, self.x, self.y],
680 })
681 }
682
683 fn render_rows(&self) -> Vec<PredictedRow> {
688 if !self.predicted_rows {
689 return Vec::new();
690 }
691
692 vec![
693 PredictedRow {
694 key_id: 1,
695 id: 2,
696 fields: vec![111.0, 222.0],
697 },
698 PredictedRow {
699 key_id: 1,
700 id: 7,
701 fields: vec![333.0],
702 },
703 PredictedRow {
704 key_id: 200,
705 id: 9,
706 fields: vec![1.0, 2.0],
707 },
708 ]
709 }
710
711 fn predicted_state(&self) -> Option<[f32; PLAYER_STATE_LEN]> {
712 self.report_state
713 .then_some([self.x, self.y, 0.0, self.vx, self.vy, 0.0, 0.0, 0.0])
714 }
715
716 fn replayed_inputs(&self) -> Option<(f64, f64, usize)> {
717 self.last_update.map(|last| (last - 50.0, last, 2))
718 }
719
720 fn apply_input(&mut self, _action: &str, _key_name: &str, _local_now: f64) {}
721
722 fn set_model(&mut self, model_name: &str) {
723 self.report_state = model_name == "predicted";
724 self.predicted_rows = model_name == "rows";
725 }
726
727 fn set_active(&mut self, active: bool) {
728 self.active = active;
729 }
730
731 fn set_map(&mut self, _map_json: &str) -> Result<(), String> {
732 Ok(())
733 }
734
735 fn sync_panel(&mut self, _items: &[String]) {}
736
737 fn reset(&mut self) {
738 self.x = 0.0;
739 self.y = 0.0;
740 self.last_update = None;
741 }
742
743 fn cycle_item(&mut self, _back: bool) {}
744
745 fn try_action(&mut self, _my_game_id: Option<u32>, _local_now: f64) -> Option<String> {
746 None
747 }
748 }
749}
750
751#[cfg(test)]
752mod tests {
753 use super::fixture::{TestClient, TestConfig};
754 use super::*;
755 use crate::client::{HOT_HAS_CAMERA, HOT_HAS_FRAMES, HOT_HAS_GAME, HOT_HAS_PREDICTED};
756 use crate::snapshot::{Block, CameraData, PlayerBlock, SnapshotPacker};
757
758 fn config_json() -> serde_json::Value {
759 serde_json::json!({
760 "timeStepMs": 1000.0 / 120.0,
761 "snapshot": {
762 "version": 3,
763 "port": 5,
764 "keys": {
765 "actor": { "id": 1, "kind": "indexed8", "class": "hot", "fields": [
766 { "name": "x", "ty": "f32", "interp": "lerp" },
767 { "name": "y", "ty": "f32", "interp": "lerp" }
768 ] }
769 }
770 },
771 "interpolation": { "delay": 100, "maxFrameAge": 1000 }
772 })
773 }
774
775 fn engine_client_config() -> EngineClientConfig {
776 serde_json::from_value(config_json()).unwrap()
777 }
778
779 fn config_with_divergence(capacity: usize) -> EngineClientConfig {
782 let mut json = config_json();
783
784 json["divergence"] = serde_json::json!({
785 "defaultThreshold": 1.0,
786 "capacity": capacity,
787 });
788
789 serde_json::from_value(json).unwrap()
790 }
791
792 fn make_state() -> ClientState<TestClient> {
793 ClientState::new(engine_client_config(), &TestConfig {})
794 }
795
796 fn frame_bytes(server_time: f64, seq: u32, x: f32, with_player: bool) -> Vec<u8> {
797 let cfg = engine_client_config();
798 let mut packer = SnapshotPacker::new(cfg.snapshot.clone());
799
800 packer
801 .pack_body(&[(
802 "actor".to_string(),
803 Block::Indexed8(vec![(2, Some(vec![FieldValue::F32(x), FieldValue::F32(0.0)]))]),
804 )])
805 .unwrap();
806
807 let camera = CameraData {
808 x,
809 y: 0.0,
810 force_reset: false,
811 shake: None,
812 };
813 let player = PlayerBlock {
814 game_id: 2,
815 input_seq: 0,
816 state: [x, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
817 centering: false,
818 };
819
820 packer
821 .pack_frame(
822 server_time,
823 seq,
824 Some(&camera),
825 with_player.then_some(&player),
826 )
827 .to_vec()
828 }
829
830 #[test]
831 fn push_frame_and_sample_writes_hot_layout() {
832 let mut state = make_state();
833
834 state.push_frame(&frame_bytes(1000.0, 1, 10.0, false), 1000.0);
835 state.push_frame(&frame_bytes(1100.0, 2, 20.0, false), 1100.0);
836
837 let len = state.sample(1150.0);
839 let hot = state.hot().to_vec();
840
841 assert_eq!(len, hot.len());
842
843 let flags = hot[0] as u32;
844
845 assert!(flags & HOT_HAS_GAME != 0);
846 assert!(flags & HOT_HAS_CAMERA != 0);
847 assert!(flags & HOT_HAS_FRAMES != 0);
848 assert!(flags & HOT_HAS_PREDICTED == 0);
849
850 assert_eq!(hot[3], 1.0);
852 assert_eq!(hot[4], 1.0);
853 assert_eq!(hot[5], 2.0);
854 assert_eq!(hot[6], 15.0);
855
856 let frames: Vec<serde_json::Value> =
857 serde_json::from_str(&state.take_frames()).unwrap();
858
859 assert_eq!(frames.len(), 1);
860 assert_eq!(state.take_frames(), "[]");
861 }
862
863 #[test]
864 fn render_overlay_appends_opaque_tail_and_sets_flag() {
865 let mut state = make_state();
866
867 state.set_active(true);
868 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
869
870 assert_eq!(state.my_game_id(), Some(2));
871
872 state.sample(1150.0);
873
874 let hot = state.hot().to_vec();
875 let flags = hot[0] as u32;
876
877 assert!(flags & HOT_HAS_PREDICTED != 0);
878
879 let tail = &hot[hot.len() - 4..];
881
882 assert_eq!(tail[1], 2.0); assert_eq!(hot[1], tail[2]); }
885
886 #[test]
887 fn reconcile_hooks_wrap_the_replay() {
888 let mut state = make_state();
889
890 state.set_active(true);
891
892 state.push_frame(&frame_bytes(1000.0, 1, 10.0, false), 1000.0);
894 assert!(state.game.reconcile_log.is_empty());
895
896 state.push_frame(&frame_bytes(1100.0, 2, 20.0, true), 1100.0);
897
898 assert_eq!(state.game.reconcile_log, vec!["begin", "state", "finish"]);
901 }
902
903 #[test]
904 fn render_rows_follow_the_tail_and_keep_schema_width() {
905 let mut state = make_state();
906
907 state.set_active(true);
908 state.set_model("rows");
909 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
910 state.push_frame(&frame_bytes(1100.0, 2, 20.0, true), 1100.0);
911 state.sample(1150.0);
912
913 let hot = state.hot().to_vec();
914
915 let rows = &hot[hot.len() - 8..];
918
919 assert_eq!(rows[0], 1.0); assert_eq!(rows[1], 2.0); assert_eq!(rows[2], 111.0);
922 assert_eq!(rows[3], 222.0);
923
924 assert_eq!(rows[4], 1.0);
926 assert_eq!(rows[5], 7.0);
927 assert_eq!(rows[6], 333.0);
928 assert_eq!(rows[7], 0.0);
929
930 assert_eq!(hot[5], 2.0);
933 assert_eq!(hot[6], 15.0);
934 }
935
936 #[test]
937 fn render_rows_default_to_empty() {
938 let mut state = make_state();
939
940 state.set_active(true);
941 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
942 state.sample(1150.0);
943
944 let hot = state.hot().to_vec();
946
947 assert_eq!(hot.len(), 3 + 1 + 4 + 1 + 4);
948 }
949
950 #[test]
951 fn reset_clears_predictor_and_frame_queue() {
952 let mut state = make_state();
953
954 state.set_active(true);
955 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
956 state.sample(1150.0);
957
958 state.reset();
959
960 assert_eq!(state.take_frames(), "[]");
961 }
962
963 #[test]
964 fn resync_clears_network_half_only() {
965 let mut state = make_state();
966
967 state.set_active(true);
968 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
969 state.push_frame(&frame_bytes(1100.0, 2, 20.0, false), 1100.0);
970 state.sample(1150.0);
971
972 state.resync();
973
974 let dump: serde_json::Value = serde_json::from_str(&state.debug_json()).unwrap();
975
976 assert_eq!(dump["interpolator"]["buffered"], 0);
977 assert!(dump["interpolator"]["lastFrame"].is_null());
978 assert!(dump["offset"].is_null());
979 assert_eq!(dump["framesOut"], 0);
980 assert_eq!(state.take_frames(), "[]");
981
982 assert_eq!(state.my_game_id(), Some(2));
984 }
985
986 #[test]
987 fn debug_json_reports_buffer_seq_window_and_offset() {
988 let mut state = make_state();
989
990 let empty: serde_json::Value = serde_json::from_str(&state.debug_json()).unwrap();
991
992 assert!(empty["myGameId"].is_null());
993 assert_eq!(empty["interpolator"]["buffered"], 0);
994 assert!(empty["interpolator"]["lastFrame"].is_null());
995
996 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
997 state.push_frame(&frame_bytes(1100.0, 2, 20.0, false), 1100.0);
998 state.sample(1150.0);
999
1000 let dump: serde_json::Value = serde_json::from_str(&state.debug_json()).unwrap();
1001
1002 assert_eq!(dump["myGameId"], 2);
1003 assert_eq!(dump["interpolator"]["buffered"], 2);
1004 assert_eq!(dump["interpolator"]["seqWindow"], serde_json::json!([1, 2]));
1005 assert_eq!(dump["interpolator"]["lastFrame"]["seq"], 2);
1006 assert_eq!(dump["interpolator"]["lastFrame"]["serverTime"], 1100.0);
1007 assert_eq!(dump["interpolator"]["delay"], 100.0);
1008 assert_eq!(dump["offset"], dump["interpolator"]["offset"]);
1009 assert_eq!(dump["hotLen"], state.hot().len());
1010 assert_eq!(dump["framesOut"], 1);
1011 }
1012
1013 #[test]
1016 fn divergence_is_off_without_config() {
1017 let mut state = make_state();
1018
1019 state.set_active(true);
1020 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
1021
1022 assert_eq!(state.take_divergence(), "null");
1023 }
1024
1025 #[test]
1028 fn divergence_falls_back_to_overlay_camera() {
1029 let mut state = ClientState::<TestClient>::new(config_with_divergence(8), &TestConfig {});
1030
1031 state.set_active(true);
1032 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
1033
1034 let dump: serde_json::Value = serde_json::from_str(&state.take_divergence()).unwrap();
1035 let record = &dump["records"][0];
1036
1037 assert_eq!(dump["samples"], 1);
1038 assert_eq!(dump["violations"], 1);
1039 assert_eq!(record["source"], "camera");
1040 assert_eq!(record["predicted"], serde_json::json!([0.0, 0.0]));
1041 assert_eq!(record["authoritative"], serde_json::json!([10.0, 0.0]));
1042 assert_eq!(record["delta"][0], -10.0);
1043 assert_eq!(record["exceeded"], serde_json::json!([0]));
1044 assert_eq!(record["thresholds"][0], 1.0);
1045 assert!(record["replayed"].is_null());
1046 }
1047
1048 #[test]
1052 fn divergence_reports_predicted_state_and_replay_window() {
1053 let mut state = ClientState::<TestClient>::new(config_with_divergence(8), &TestConfig {});
1054
1055 state.set_active(true);
1056 state.set_model("predicted");
1057
1058 state.push_frame(&frame_bytes(1000.0, 1, 0.0, true), 1000.0);
1060 state.sample(1100.0);
1061 state.push_frame(&frame_bytes(1100.0, 2, 50.0, true), 1100.0);
1062
1063 let dump: serde_json::Value = serde_json::from_str(&state.take_divergence()).unwrap();
1064
1065 assert_eq!(dump["samples"], 2);
1066 assert_eq!(dump["violations"], 1);
1067 assert_eq!(dump["records"].as_array().unwrap().len(), 1);
1068 assert_eq!(dump["maxDelta"][0], 50.0);
1069
1070 let record = &dump["records"][0];
1071
1072 assert_eq!(record["source"], "state");
1073 assert_eq!(record["serverTime"], 1100.0);
1074 assert_eq!(record["localNow"], 1100.0);
1075 assert_eq!(record["inputSeq"], 0);
1076 assert_eq!(record["delta"][0], -50.0);
1077 assert_eq!(record["exceeded"], serde_json::json!([0]));
1078 assert_eq!(record["replayed"]["from"], 1050.0);
1079 assert_eq!(record["replayed"]["to"], 1100.0);
1080 assert_eq!(record["replayed"]["count"], 2);
1081
1082 let drained: serde_json::Value = serde_json::from_str(&state.take_divergence()).unwrap();
1084
1085 assert_eq!(drained["records"].as_array().unwrap().len(), 0);
1086 assert_eq!(drained["samples"], 2);
1087 assert_eq!(drained["maxDelta"][0], 50.0);
1088 }
1089
1090 #[test]
1091 fn divergence_ring_buffer_evicts_oldest_records() {
1092 let mut state = ClientState::<TestClient>::new(config_with_divergence(1), &TestConfig {});
1093
1094 state.set_active(true);
1095 state.set_model("predicted");
1096 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
1097 state.push_frame(&frame_bytes(1100.0, 2, 90.0, true), 1100.0);
1098
1099 let dump: serde_json::Value = serde_json::from_str(&state.take_divergence()).unwrap();
1100
1101 assert_eq!(dump["violations"], 2);
1102 assert_eq!(dump["dropped"], 1);
1103 assert_eq!(dump["records"].as_array().unwrap().len(), 1);
1104 assert_eq!(dump["records"][0]["serverTime"], 1100.0);
1105 }
1106
1107 #[test]
1110 fn divergence_zero_capacity_counts_evictions_honestly() {
1111 let mut state = ClientState::<TestClient>::new(config_with_divergence(0), &TestConfig {});
1112
1113 state.set_active(true);
1114 state.set_model("predicted");
1115 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
1116 state.push_frame(&frame_bytes(1100.0, 2, 90.0, true), 1100.0);
1117
1118 let dump: serde_json::Value = serde_json::from_str(&state.take_divergence()).unwrap();
1119
1120 assert_eq!(dump["violations"], 2);
1121 assert_eq!(dump["dropped"], 1);
1122 assert_eq!(dump["records"].as_array().unwrap().len(), 1);
1123 }
1124
1125 #[test]
1131 fn second_schema_key_of_different_block_kind_flows_into_hot_buffer() {
1132 let config = serde_json::json!({
1133 "timeStepMs": 1000.0 / 120.0,
1134 "snapshot": {
1135 "version": 3,
1136 "port": 5,
1137 "keys": {
1138 "actor": { "id": 1, "kind": "indexed8", "class": "hot", "fields": [
1139 { "name": "x", "ty": "f32", "interp": "lerp" },
1140 { "name": "y", "ty": "f32", "interp": "lerp" }
1141 ] },
1142 "zone": { "id": 2, "kind": "indexedNoNull8", "class": "hot", "fields": [
1143 { "name": "level", "ty": "f32", "interp": "discrete" }
1144 ] }
1145 }
1146 },
1147 "interpolation": { "delay": 100, "maxFrameAge": 1000 }
1148 });
1149 let cfg: EngineClientConfig = serde_json::from_value(config).unwrap();
1150 let mut state = ClientState::<TestClient>::new(cfg.clone(), &TestConfig {});
1151 let mut packer = SnapshotPacker::new(cfg.snapshot.clone());
1152
1153 packer
1154 .pack_body(&[
1155 (
1156 "actor".to_string(),
1157 Block::Indexed8(vec![(2, Some(vec![FieldValue::F32(10.0), FieldValue::F32(0.0)]))]),
1158 ),
1159 (
1160 "zone".to_string(),
1161 Block::IndexedNoNull8(vec![(0, vec![FieldValue::F32(7.0)])]),
1162 ),
1163 ])
1164 .unwrap();
1165
1166 let frame = packer.pack_frame(1000.0, 1, None, None).to_vec();
1167
1168 state.push_frame(&frame, 1000.0);
1169 state.push_frame(&frame, 1100.0);
1170 state.sample(1150.0);
1171
1172 let hot = state.hot().to_vec();
1173
1174 assert_eq!(hot[3], 1.0); assert_eq!(hot[8], 1.0); assert_eq!(hot[9], 2.0); assert_eq!(hot[11], 7.0); }
1180
1181 #[test]
1182 fn reset_clears_my_game_id() {
1183 let mut state = make_state();
1184
1185 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
1186 state.push_frame(&frame_bytes(1100.0, 2, 20.0, true), 1100.0);
1187 state.sample(1150.0);
1188 assert_eq!(state.my_game_id(), Some(2));
1189
1190 state.reset();
1193 assert_eq!(state.my_game_id(), None);
1194 }
1195}