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 row_widths: Box<[Option<u16>; 256]>,
200}
201
202impl<G: GameClientDef> ClientState<G> {
203 pub fn new(cfg: EngineClientConfig, game_cfg: &G::Config) -> Self {
204 let interpolator = Interpolator::new(&cfg.interpolation, cfg.snapshot.clone());
205 let game = G::new(game_cfg, &cfg);
206 let divergence = cfg.divergence.clone().map(DivergenceTracker::new);
207
208 let mut row_widths = Box::new([None; 256]);
209
210 for schema in cfg.snapshot.keys.values() {
211 row_widths[schema.id as usize] = Some(schema.fields.len() as u16);
212 }
213
214 Self {
215 cfg,
216 interpolator,
217 game,
218 my_game_id: None,
219 frames_out: Vec::new(),
220 hot: Vec::new(),
221 divergence,
222 row_widths,
223 }
224 }
225
226 pub fn push_frame(&mut self, data: &[u8], local_now: f64) -> bool {
230 let frame = match unpack::unpack_frame(data, &self.cfg.snapshot) {
231 Ok(frame) => frame,
232 Err(UnpackError::WrongVersion | UnpackError::Truncated) => return false,
233 };
234
235 if frame.port != self.cfg.snapshot.port {
236 return false;
237 }
238
239 if frame.player.is_some() {
243 self.game.begin_reconcile(&frame.snapshot);
244 }
245
246 self.interpolator.push(
247 FrameData {
248 snapshot: frame.snapshot,
249 camera: frame.camera,
250 },
251 frame.server_time,
252 local_now,
253 frame.seq,
254 );
255
256 if let Some(player) = frame.player {
257 self.my_game_id = Some(player.game_id as u32);
258
259 let offset = self.interpolator.offset().unwrap_or(0.0);
261
262 self.observe_divergence(&player, frame.server_time, offset, local_now);
263
264 self.game.on_server_state(
265 player.state,
266 player.centering,
267 frame.server_time,
268 offset,
269 local_now,
270 );
271
272 self.game.finish_reconcile();
274 }
275
276 true
277 }
278
279 pub fn game(&self) -> &G {
282 &self.game
283 }
284
285 pub fn my_game_id(&self) -> Option<u32> {
286 self.my_game_id
287 }
288
289 pub fn offset(&self) -> Option<f64> {
290 self.interpolator.offset()
291 }
292
293 pub fn sample(&mut self, local_now: f64) -> usize {
297 let result = self.interpolator.sample(local_now);
298
299 for frame in result.frames {
301 self.game.track_frame(self.my_game_id, &frame);
302
303 let mut game = unpack::snapshot_to_json(&frame.snapshot);
304
305 self.game
306 .filter_frame_game(&mut game, self.my_game_id, local_now);
307
308 self.frames_out.push(json!({
309 "game": game,
310 "camera": unpack::camera_to_json(frame.camera.as_ref()),
311 }));
312
313 self.game.update_world(&frame.snapshot);
314 }
315
316 if let Some(game) = &result.game {
317 self.game.update_world_interpolated(game);
318 }
319
320 self.game.update(local_now);
321
322 let overlay = self.game.render_overlay(self.my_game_id);
323 let rows = self.game.render_rows();
324
325 self.write_hot(result.game.as_ref(), result.camera, overlay.as_ref(), &rows);
326 self.hot.len()
327 }
328
329 pub fn hot(&self) -> &[f32] {
330 &self.hot
331 }
332
333 pub fn take_frames(&mut self) -> String {
335 let frames = std::mem::take(&mut self.frames_out);
336
337 serde_json::to_string(&frames).unwrap_or_else(|_| "[]".to_string())
338 }
339
340 pub fn apply_input(&mut self, action: &str, key_name: &str, local_now: f64) {
341 self.game.apply_input(action, key_name, local_now);
342 }
343
344 pub fn apply_aim(&mut self, x: f32, y: f32, flags: u32, local_now: f64) {
345 self.game.apply_aim(x, y, flags, local_now);
346 }
347
348 pub fn try_action(&mut self, local_now: f64) -> Option<String> {
349 self.game.try_action(self.my_game_id, local_now)
350 }
351
352 pub fn cycle_item(&mut self, back: bool) {
353 self.game.cycle_item(back);
354 }
355
356 pub fn set_model(&mut self, model_name: &str) {
357 self.game.set_model(model_name);
358 }
359
360 pub fn set_active(&mut self, active: bool) {
362 self.game.set_active(active);
363 }
364
365 pub fn set_map(&mut self, map_json: &str) -> Result<(), String> {
367 self.interpolator.reset();
368 self.frames_out.clear();
369 self.game.set_map(map_json)
370 }
371
372 pub fn sync_panel(&mut self, panel_json: &str) {
373 let Ok(Value::Array(items)) = serde_json::from_str(panel_json) else {
374 return;
375 };
376
377 let items: Vec<String> = items
378 .iter()
379 .map(|item| match item {
380 Value::String(s) => s.clone(),
381 other => other.to_string(),
382 })
383 .collect();
384
385 self.game.sync_panel(&items);
386 }
387
388 pub fn reset(&mut self) {
390 self.interpolator.reset();
391 self.game.reset();
392 self.frames_out.clear();
393 self.my_game_id = None;
397 }
398
399 pub fn resync(&mut self) {
405 self.interpolator.reset();
406 self.frames_out.clear();
407 }
408
409 pub fn debug_json(&self) -> String {
413 json!({
414 "myGameId": self.my_game_id,
415 "offset": self.interpolator.offset(),
416 "interpolator": self.interpolator.debug_json(),
417 "hotLen": self.hot.len(),
418 "framesOut": self.frames_out.len(),
419 })
420 .to_string()
421 }
422
423 pub fn take_divergence(&mut self) -> String {
427 match &mut self.divergence {
428 Some(tracker) => tracker.take_json(),
429 None => "null".to_string(),
430 }
431 }
432
433 fn observe_divergence(
436 &mut self,
437 player: &unpack::DecodedPlayer,
438 server_time: f64,
439 offset: f64,
440 local_now: f64,
441 ) {
442 let Some(tracker) = &mut self.divergence else {
443 return;
444 };
445
446 let (source, predicted) = match self.game.predicted_state() {
448 Some(state) => (Source::State, state.to_vec()),
449 None => match self.game.render_overlay(self.my_game_id) {
450 Some(overlay) => (Source::Camera, overlay.camera.to_vec()),
451 None => return,
452 },
453 };
454
455 tracker.observe(Observation {
456 source,
457 predicted: &predicted,
458 authoritative: &player.state,
459 server_time,
460 local_now,
461 offset,
462 input_seq: player.input_seq,
463 replayed: self.game.replayed_inputs(),
464 });
465 }
466
467 pub fn decode_frame(&self, data: &[u8]) -> String {
469 match unpack::unpack_frame(data, &self.cfg.snapshot) {
470 Ok(frame) => unpack::frame_to_json(&frame).to_string(),
471 Err(_) => "null".to_string(),
472 }
473 }
474
475 fn write_hot(
483 &mut self,
484 game: Option<&InterpolatedGame>,
485 camera: Option<[f32; 2]>,
486 overlay: Option<&RenderOverlay>,
487 rows: &[PredictedRow],
488 ) {
489 self.hot.clear();
490
491 let mut flags = 0u32;
492
493 if game.is_some() {
494 flags |= super::HOT_HAS_GAME;
495 }
496
497 if !self.frames_out.is_empty() {
498 flags |= super::HOT_HAS_FRAMES;
499 }
500
501 if overlay.is_some() || !rows.is_empty() {
506 flags |= super::HOT_HAS_PREDICTED;
507 }
508
509 let camera = overlay.map(|o| o.camera).or(camera);
511
512 if camera.is_some() {
513 flags |= super::HOT_HAS_CAMERA;
514 }
515
516 self.hot.push(flags as f32);
517
518 let camera = camera.unwrap_or([0.0, 0.0]);
519
520 self.hot.push(camera[0]);
521 self.hot.push(camera[1]);
522
523 let empty = InterpolatedGame::default();
524 let game = game.unwrap_or(&empty);
525
526 let tank_count: usize = blocks_of_kind(&self.cfg.snapshot, game, BlockKind::Indexed8)
531 .map(|(_, rows)| rows.len())
532 .sum();
533
534 self.hot.push(tank_count as f32);
535
536 for (key_id, rows) in blocks_of_kind(&self.cfg.snapshot, game, BlockKind::Indexed8) {
537 for row in rows {
538 self.hot.push(key_id as f32);
539 self.hot.push(row.id as f32);
540
541 for field in &row.fields {
542 self.hot.push(field_as_f32(*field));
543 }
544 }
545 }
546
547 let dynamic_count: usize =
548 blocks_of_kind(&self.cfg.snapshot, game, BlockKind::IndexedNoNull8)
549 .map(|(_, rows)| rows.len())
550 .sum();
551
552 self.hot.push(dynamic_count as f32);
553
554 for (key_id, rows) in blocks_of_kind(&self.cfg.snapshot, game, BlockKind::IndexedNoNull8) {
555 for row in rows {
556 self.hot.push(key_id as f32);
557 self.hot.push(row.id as f32);
558
559 for field in &row.fields {
560 self.hot.push(field_as_f32(*field));
561 }
562 }
563 }
564
565 if let Some(overlay) = overlay {
566 self.hot.extend_from_slice(&overlay.tail);
567 }
568
569 for row in rows {
570 let Some(width) = self.row_widths[row.key_id as usize] else {
574 continue;
575 };
576
577 let width = width as usize;
578
579 self.hot.push(row.key_id as f32);
580 self.hot.push(row.id as f32);
581
582 for index in 0..width {
583 self.hot.push(row.fields.get(index).copied().unwrap_or(0.0));
584 }
585 }
586 }
587}
588
589#[cfg(test)]
595mod fixture {
596 use super::*;
597 use serde::Deserialize;
598
599 #[derive(Deserialize)]
600 pub struct TestConfig {}
601
602 pub struct TestClient {
603 x: f32,
604 y: f32,
605 vx: f32,
606 vy: f32,
607 active: bool,
608 alive: bool,
609 last_update: Option<f64>,
610 report_state: bool,
613 predicted_rows: bool,
615 pub reconcile_log: Vec<&'static str>,
617 }
618
619 impl GameClientDef for TestClient {
620 type Config = TestConfig;
621
622 fn new(_cfg: &Self::Config, _engine_cfg: &EngineClientConfig) -> Self {
623 Self {
624 x: 0.0,
625 y: 0.0,
626 vx: 0.0,
627 vy: 0.0,
628 active: false,
629 alive: true,
630 last_update: None,
631 report_state: false,
632 predicted_rows: false,
633 reconcile_log: Vec::new(),
634 }
635 }
636
637 fn on_server_state(
638 &mut self,
639 state: [f32; PLAYER_STATE_LEN],
640 _centering: bool,
641 _server_time: f64,
642 _offset: f64,
643 _local_now: f64,
644 ) {
645 self.x = state[0];
646 self.y = state[1];
647 self.vx = state[3];
648 self.vy = state[4];
649 self.reconcile_log.push("state");
650 }
651
652 fn begin_reconcile(&mut self, _snapshot: &DecodedSnapshot) {
653 self.reconcile_log.push("begin");
654 }
655
656 fn finish_reconcile(&mut self) {
657 self.reconcile_log.push("finish");
658 }
659
660 fn update(&mut self, local_now: f64) {
661 let dt = self
662 .last_update
663 .map(|last| (local_now - last) / 1000.0)
664 .unwrap_or(0.0) as f32;
665
666 self.x += self.vx * dt;
667 self.y += self.vy * dt;
668 self.last_update = Some(local_now);
669 }
670
671 fn track_frame(&mut self, _my_game_id: Option<u32>, _frame: &FrameData) {}
672
673 fn filter_frame_game(
674 &mut self,
675 _game: &mut Map<String, Value>,
676 _my_game_id: Option<u32>,
677 _local_now: f64,
678 ) {
679 }
680
681 fn update_world(&mut self, _snapshot: &DecodedSnapshot) {}
682
683 fn update_world_interpolated(&mut self, _game: &InterpolatedGame) {}
684
685 fn render_overlay(&self, my_game_id: Option<u32>) -> Option<RenderOverlay> {
686 let game_id = my_game_id?;
687
688 (self.active && self.alive).then(|| RenderOverlay {
689 camera: [self.x, self.y],
690 tail: vec![0.0, game_id as f32, self.x, self.y],
691 })
692 }
693
694 fn render_rows(&self) -> Vec<PredictedRow> {
699 if !self.predicted_rows {
700 return Vec::new();
701 }
702
703 vec![
704 PredictedRow {
705 key_id: 1,
706 id: 2,
707 fields: vec![111.0, 222.0],
708 },
709 PredictedRow {
710 key_id: 1,
711 id: 7,
712 fields: vec![333.0],
713 },
714 PredictedRow {
715 key_id: 200,
716 id: 9,
717 fields: vec![1.0, 2.0],
718 },
719 ]
720 }
721
722 fn predicted_state(&self) -> Option<[f32; PLAYER_STATE_LEN]> {
723 self.report_state
724 .then_some([self.x, self.y, 0.0, self.vx, self.vy, 0.0, 0.0, 0.0])
725 }
726
727 fn replayed_inputs(&self) -> Option<(f64, f64, usize)> {
728 self.last_update.map(|last| (last - 50.0, last, 2))
729 }
730
731 fn apply_input(&mut self, _action: &str, _key_name: &str, _local_now: f64) {}
732
733 fn set_model(&mut self, model_name: &str) {
734 self.report_state = model_name == "predicted";
735 self.predicted_rows = model_name == "rows";
736 }
737
738 fn set_active(&mut self, active: bool) {
739 self.active = active;
740 }
741
742 fn set_map(&mut self, _map_json: &str) -> Result<(), String> {
743 Ok(())
744 }
745
746 fn sync_panel(&mut self, _items: &[String]) {}
747
748 fn reset(&mut self) {
749 self.x = 0.0;
750 self.y = 0.0;
751 self.last_update = None;
752 }
753
754 fn cycle_item(&mut self, _back: bool) {}
755
756 fn try_action(&mut self, _my_game_id: Option<u32>, _local_now: f64) -> Option<String> {
757 None
758 }
759 }
760}
761
762#[cfg(test)]
763mod tests {
764 use super::fixture::{TestClient, TestConfig};
765 use super::*;
766 use crate::client::{HOT_HAS_CAMERA, HOT_HAS_FRAMES, HOT_HAS_GAME, HOT_HAS_PREDICTED};
767 use crate::snapshot::{Block, CameraData, PlayerBlock, SnapshotPacker};
768
769 fn config_json() -> serde_json::Value {
770 serde_json::json!({
771 "timeStepMs": 1000.0 / 120.0,
772 "snapshot": {
773 "version": 3,
774 "port": 5,
775 "keys": {
776 "actor": { "id": 1, "kind": "indexed8", "class": "hot", "fields": [
777 { "name": "x", "ty": "f32", "interp": "lerp" },
778 { "name": "y", "ty": "f32", "interp": "lerp" }
779 ] }
780 }
781 },
782 "interpolation": { "delay": 100, "maxFrameAge": 1000 }
783 })
784 }
785
786 fn engine_client_config() -> EngineClientConfig {
787 serde_json::from_value(config_json()).unwrap()
788 }
789
790 fn config_with_divergence(capacity: usize) -> EngineClientConfig {
793 let mut json = config_json();
794
795 json["divergence"] = serde_json::json!({
796 "defaultThreshold": 1.0,
797 "capacity": capacity,
798 });
799
800 serde_json::from_value(json).unwrap()
801 }
802
803 fn make_state() -> ClientState<TestClient> {
804 ClientState::new(engine_client_config(), &TestConfig {})
805 }
806
807 fn frame_bytes(server_time: f64, seq: u32, x: f32, with_player: bool) -> Vec<u8> {
808 let cfg = engine_client_config();
809 let mut packer = SnapshotPacker::new(cfg.snapshot.clone());
810
811 packer
812 .pack_body(&[(
813 "actor".to_string(),
814 Block::Indexed8(vec![(2, Some(vec![FieldValue::F32(x), FieldValue::F32(0.0)]))]),
815 )])
816 .unwrap();
817
818 let camera = CameraData {
819 x,
820 y: 0.0,
821 force_reset: false,
822 shake: None,
823 };
824 let player = PlayerBlock {
825 game_id: 2,
826 input_seq: 0,
827 state: [x, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
828 centering: false,
829 };
830
831 packer
832 .pack_frame(
833 server_time,
834 seq,
835 Some(&camera),
836 with_player.then_some(&player),
837 )
838 .to_vec()
839 }
840
841 #[test]
842 fn push_frame_and_sample_writes_hot_layout() {
843 let mut state = make_state();
844
845 state.push_frame(&frame_bytes(1000.0, 1, 10.0, false), 1000.0);
846 state.push_frame(&frame_bytes(1100.0, 2, 20.0, false), 1100.0);
847
848 let len = state.sample(1150.0);
850 let hot = state.hot().to_vec();
851
852 assert_eq!(len, hot.len());
853
854 let flags = hot[0] as u32;
855
856 assert!(flags & HOT_HAS_GAME != 0);
857 assert!(flags & HOT_HAS_CAMERA != 0);
858 assert!(flags & HOT_HAS_FRAMES != 0);
859 assert!(flags & HOT_HAS_PREDICTED == 0);
860
861 assert_eq!(hot[3], 1.0);
863 assert_eq!(hot[4], 1.0);
864 assert_eq!(hot[5], 2.0);
865 assert_eq!(hot[6], 15.0);
866
867 let frames: Vec<serde_json::Value> =
868 serde_json::from_str(&state.take_frames()).unwrap();
869
870 assert_eq!(frames.len(), 1);
871 assert_eq!(state.take_frames(), "[]");
872 }
873
874 #[test]
875 fn render_overlay_appends_opaque_tail_and_sets_flag() {
876 let mut state = make_state();
877
878 state.set_active(true);
879 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
880
881 assert_eq!(state.my_game_id(), Some(2));
882
883 state.sample(1150.0);
884
885 let hot = state.hot().to_vec();
886 let flags = hot[0] as u32;
887
888 assert!(flags & HOT_HAS_PREDICTED != 0);
889
890 let tail = &hot[hot.len() - 4..];
892
893 assert_eq!(tail[1], 2.0); assert_eq!(hot[1], tail[2]); }
896
897 #[test]
898 fn reconcile_hooks_wrap_the_replay() {
899 let mut state = make_state();
900
901 state.set_active(true);
902
903 state.push_frame(&frame_bytes(1000.0, 1, 10.0, false), 1000.0);
905 assert!(state.game.reconcile_log.is_empty());
906
907 state.push_frame(&frame_bytes(1100.0, 2, 20.0, true), 1100.0);
908
909 assert_eq!(state.game.reconcile_log, vec!["begin", "state", "finish"]);
912 }
913
914 #[test]
915 fn render_rows_follow_the_tail_and_keep_schema_width() {
916 let mut state = make_state();
917
918 state.set_active(true);
919 state.set_model("rows");
920 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
921 state.push_frame(&frame_bytes(1100.0, 2, 20.0, true), 1100.0);
922 state.sample(1150.0);
923
924 let hot = state.hot().to_vec();
925
926 let rows = &hot[hot.len() - 8..];
929
930 assert_eq!(rows[0], 1.0); assert_eq!(rows[1], 2.0); assert_eq!(rows[2], 111.0);
933 assert_eq!(rows[3], 222.0);
934
935 assert_eq!(rows[4], 1.0);
937 assert_eq!(rows[5], 7.0);
938 assert_eq!(rows[6], 333.0);
939 assert_eq!(rows[7], 0.0);
940
941 assert_eq!(hot[5], 2.0);
944 assert_eq!(hot[6], 15.0);
945 }
946
947 #[test]
948 fn game_rows_alone_still_raise_the_tail_flag() {
949 let mut state = make_state();
955
956 state.set_active(true);
957 state.set_model("rows");
958 state.push_frame(&frame_bytes(1000.0, 1, 10.0, false), 1000.0);
959 state.push_frame(&frame_bytes(1100.0, 2, 20.0, false), 1100.0);
960 state.sample(1150.0);
961
962 let hot = state.hot().to_vec();
963 let flags = hot[0] as u32;
964
965 assert!(flags & HOT_HAS_PREDICTED != 0);
966
967 let rows = &hot[hot.len() - 8..];
969
970 assert_eq!(rows[0], 1.0);
971 assert_eq!(rows[1], 2.0);
972 assert_eq!(rows[4], 1.0);
973 assert_eq!(rows[5], 7.0);
974 }
975
976 #[test]
977 fn render_rows_default_to_empty() {
978 let mut state = make_state();
979
980 state.set_active(true);
981 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
982 state.sample(1150.0);
983
984 let hot = state.hot().to_vec();
986
987 assert_eq!(hot.len(), 3 + 1 + 4 + 1 + 4);
988 }
989
990 #[test]
991 fn reset_clears_predictor_and_frame_queue() {
992 let mut state = make_state();
993
994 state.set_active(true);
995 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
996 state.sample(1150.0);
997
998 state.reset();
999
1000 assert_eq!(state.take_frames(), "[]");
1001 }
1002
1003 #[test]
1004 fn resync_clears_network_half_only() {
1005 let mut state = make_state();
1006
1007 state.set_active(true);
1008 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
1009 state.push_frame(&frame_bytes(1100.0, 2, 20.0, false), 1100.0);
1010 state.sample(1150.0);
1011
1012 state.resync();
1013
1014 let dump: serde_json::Value = serde_json::from_str(&state.debug_json()).unwrap();
1015
1016 assert_eq!(dump["interpolator"]["buffered"], 0);
1017 assert!(dump["interpolator"]["lastFrame"].is_null());
1018 assert!(dump["offset"].is_null());
1019 assert_eq!(dump["framesOut"], 0);
1020 assert_eq!(state.take_frames(), "[]");
1021
1022 assert_eq!(state.my_game_id(), Some(2));
1024 }
1025
1026 #[test]
1027 fn debug_json_reports_buffer_seq_window_and_offset() {
1028 let mut state = make_state();
1029
1030 let empty: serde_json::Value = serde_json::from_str(&state.debug_json()).unwrap();
1031
1032 assert!(empty["myGameId"].is_null());
1033 assert_eq!(empty["interpolator"]["buffered"], 0);
1034 assert!(empty["interpolator"]["lastFrame"].is_null());
1035
1036 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
1037 state.push_frame(&frame_bytes(1100.0, 2, 20.0, false), 1100.0);
1038 state.sample(1150.0);
1039
1040 let dump: serde_json::Value = serde_json::from_str(&state.debug_json()).unwrap();
1041
1042 assert_eq!(dump["myGameId"], 2);
1043 assert_eq!(dump["interpolator"]["buffered"], 2);
1044 assert_eq!(dump["interpolator"]["seqWindow"], serde_json::json!([1, 2]));
1045 assert_eq!(dump["interpolator"]["lastFrame"]["seq"], 2);
1046 assert_eq!(dump["interpolator"]["lastFrame"]["serverTime"], 1100.0);
1047 assert_eq!(dump["interpolator"]["delay"], 100.0);
1048 assert_eq!(dump["offset"], dump["interpolator"]["offset"]);
1049 assert_eq!(dump["hotLen"], state.hot().len());
1050 assert_eq!(dump["framesOut"], 1);
1051 }
1052
1053 #[test]
1056 fn divergence_is_off_without_config() {
1057 let mut state = make_state();
1058
1059 state.set_active(true);
1060 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
1061
1062 assert_eq!(state.take_divergence(), "null");
1063 }
1064
1065 #[test]
1068 fn divergence_falls_back_to_overlay_camera() {
1069 let mut state = ClientState::<TestClient>::new(config_with_divergence(8), &TestConfig {});
1070
1071 state.set_active(true);
1072 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
1073
1074 let dump: serde_json::Value = serde_json::from_str(&state.take_divergence()).unwrap();
1075 let record = &dump["records"][0];
1076
1077 assert_eq!(dump["samples"], 1);
1078 assert_eq!(dump["violations"], 1);
1079 assert_eq!(record["source"], "camera");
1080 assert_eq!(record["predicted"], serde_json::json!([0.0, 0.0]));
1081 assert_eq!(record["authoritative"], serde_json::json!([10.0, 0.0]));
1082 assert_eq!(record["delta"][0], -10.0);
1083 assert_eq!(record["exceeded"], serde_json::json!([0]));
1084 assert_eq!(record["thresholds"][0], 1.0);
1085 assert!(record["replayed"].is_null());
1086 }
1087
1088 #[test]
1092 fn divergence_reports_predicted_state_and_replay_window() {
1093 let mut state = ClientState::<TestClient>::new(config_with_divergence(8), &TestConfig {});
1094
1095 state.set_active(true);
1096 state.set_model("predicted");
1097
1098 state.push_frame(&frame_bytes(1000.0, 1, 0.0, true), 1000.0);
1100 state.sample(1100.0);
1101 state.push_frame(&frame_bytes(1100.0, 2, 50.0, true), 1100.0);
1102
1103 let dump: serde_json::Value = serde_json::from_str(&state.take_divergence()).unwrap();
1104
1105 assert_eq!(dump["samples"], 2);
1106 assert_eq!(dump["violations"], 1);
1107 assert_eq!(dump["records"].as_array().unwrap().len(), 1);
1108 assert_eq!(dump["maxDelta"][0], 50.0);
1109
1110 let record = &dump["records"][0];
1111
1112 assert_eq!(record["source"], "state");
1113 assert_eq!(record["serverTime"], 1100.0);
1114 assert_eq!(record["localNow"], 1100.0);
1115 assert_eq!(record["inputSeq"], 0);
1116 assert_eq!(record["delta"][0], -50.0);
1117 assert_eq!(record["exceeded"], serde_json::json!([0]));
1118 assert_eq!(record["replayed"]["from"], 1050.0);
1119 assert_eq!(record["replayed"]["to"], 1100.0);
1120 assert_eq!(record["replayed"]["count"], 2);
1121
1122 let drained: serde_json::Value = serde_json::from_str(&state.take_divergence()).unwrap();
1124
1125 assert_eq!(drained["records"].as_array().unwrap().len(), 0);
1126 assert_eq!(drained["samples"], 2);
1127 assert_eq!(drained["maxDelta"][0], 50.0);
1128 }
1129
1130 #[test]
1131 fn divergence_ring_buffer_evicts_oldest_records() {
1132 let mut state = ClientState::<TestClient>::new(config_with_divergence(1), &TestConfig {});
1133
1134 state.set_active(true);
1135 state.set_model("predicted");
1136 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
1137 state.push_frame(&frame_bytes(1100.0, 2, 90.0, true), 1100.0);
1138
1139 let dump: serde_json::Value = serde_json::from_str(&state.take_divergence()).unwrap();
1140
1141 assert_eq!(dump["violations"], 2);
1142 assert_eq!(dump["dropped"], 1);
1143 assert_eq!(dump["records"].as_array().unwrap().len(), 1);
1144 assert_eq!(dump["records"][0]["serverTime"], 1100.0);
1145 }
1146
1147 #[test]
1150 fn divergence_zero_capacity_counts_evictions_honestly() {
1151 let mut state = ClientState::<TestClient>::new(config_with_divergence(0), &TestConfig {});
1152
1153 state.set_active(true);
1154 state.set_model("predicted");
1155 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
1156 state.push_frame(&frame_bytes(1100.0, 2, 90.0, true), 1100.0);
1157
1158 let dump: serde_json::Value = serde_json::from_str(&state.take_divergence()).unwrap();
1159
1160 assert_eq!(dump["violations"], 2);
1161 assert_eq!(dump["dropped"], 1);
1162 assert_eq!(dump["records"].as_array().unwrap().len(), 1);
1163 }
1164
1165 #[test]
1171 fn second_schema_key_of_different_block_kind_flows_into_hot_buffer() {
1172 let config = serde_json::json!({
1173 "timeStepMs": 1000.0 / 120.0,
1174 "snapshot": {
1175 "version": 3,
1176 "port": 5,
1177 "keys": {
1178 "actor": { "id": 1, "kind": "indexed8", "class": "hot", "fields": [
1179 { "name": "x", "ty": "f32", "interp": "lerp" },
1180 { "name": "y", "ty": "f32", "interp": "lerp" }
1181 ] },
1182 "zone": { "id": 2, "kind": "indexedNoNull8", "class": "hot", "fields": [
1183 { "name": "level", "ty": "f32", "interp": "discrete" }
1184 ] }
1185 }
1186 },
1187 "interpolation": { "delay": 100, "maxFrameAge": 1000 }
1188 });
1189 let cfg: EngineClientConfig = serde_json::from_value(config).unwrap();
1190 let mut state = ClientState::<TestClient>::new(cfg.clone(), &TestConfig {});
1191 let mut packer = SnapshotPacker::new(cfg.snapshot.clone());
1192
1193 packer
1194 .pack_body(&[
1195 (
1196 "actor".to_string(),
1197 Block::Indexed8(vec![(2, Some(vec![FieldValue::F32(10.0), FieldValue::F32(0.0)]))]),
1198 ),
1199 (
1200 "zone".to_string(),
1201 Block::IndexedNoNull8(vec![(0, vec![FieldValue::F32(7.0)])]),
1202 ),
1203 ])
1204 .unwrap();
1205
1206 let frame = packer.pack_frame(1000.0, 1, None, None).to_vec();
1207
1208 state.push_frame(&frame, 1000.0);
1209 state.push_frame(&frame, 1100.0);
1210 state.sample(1150.0);
1211
1212 let hot = state.hot().to_vec();
1213
1214 assert_eq!(hot[3], 1.0); assert_eq!(hot[8], 1.0); assert_eq!(hot[9], 2.0); assert_eq!(hot[11], 7.0); }
1220
1221 #[test]
1222 fn reset_clears_my_game_id() {
1223 let mut state = make_state();
1224
1225 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
1226 state.push_frame(&frame_bytes(1100.0, 2, 20.0, true), 1100.0);
1227 state.sample(1150.0);
1228 assert_eq!(state.my_game_id(), Some(2));
1229
1230 state.reset();
1233 assert_eq!(state.my_game_id(), None);
1234 }
1235}