1use serde_json::{Map, Value, json};
17
18use super::divergence::{DivergenceTracker, Observation, Source};
19use super::interpolator::{FrameData, InterpolatedGame, Interpolator};
20use super::unpack::{self, DecodedSnapshot, UnpackError};
21use crate::config::{BlockKind, EngineClientConfig, FieldValue, PLAYER_STATE_LEN, SnapshotConfig};
22
23pub struct RenderOverlay {
26 pub camera: [f32; 2],
27 pub tail: Vec<f32>,
28}
29
30pub trait GameClientDef: Sized {
35 type Config: serde::de::DeserializeOwned;
36
37 fn new(cfg: &Self::Config, engine_cfg: &EngineClientConfig) -> Self;
38
39 fn on_server_state(
44 &mut self,
45 state: [f32; PLAYER_STATE_LEN],
46 centering: bool,
47 server_time: f64,
48 offset: f64,
49 local_now: f64,
50 );
51
52 fn set_server_offset(&mut self, offset: Option<f64>);
54
55 fn update(&mut self, local_now: f64);
57
58 fn track_frame(&mut self, my_game_id: Option<u32>, frame: &FrameData);
62
63 fn filter_frame_game(&mut self, game: &mut Map<String, Value>, my_game_id: Option<u32>, local_now: f64);
66
67 fn update_world(&mut self, snapshot: &DecodedSnapshot);
70
71 fn update_world_interpolated(&mut self, game: &InterpolatedGame);
73
74 fn render_overlay(&self, my_game_id: Option<u32>) -> Option<RenderOverlay>;
78
79 fn predicted_state(&self) -> Option<[f32; PLAYER_STATE_LEN]> {
86 None
87 }
88
89 fn replayed_inputs(&self) -> Option<(f64, f64, usize)> {
94 None
95 }
96
97 fn apply_input(&mut self, action: &str, key_name: &str, local_now: f64);
98 fn set_model(&mut self, model_name: &str);
99 fn set_active(&mut self, active: bool);
100 fn set_map(&mut self, map_json: &str) -> Result<(), String>;
101 fn sync_panel(&mut self, items: &[String]);
102 fn reset(&mut self);
103
104 fn cycle_item(&mut self, back: bool);
107
108 fn try_action(&mut self, my_game_id: Option<u32>, local_now: f64) -> Option<String>;
111}
112
113fn field_as_f32(value: FieldValue) -> f32 {
115 match value {
116 FieldValue::F32(v) => v,
117 FieldValue::U8(v) => v as f32,
118 FieldValue::U16(v) => v as f32,
119 FieldValue::U32(v) => v as f32,
120 }
121}
122
123fn blocks_of_kind<'a>(
126 snapshot_cfg: &'a SnapshotConfig,
127 game: &'a InterpolatedGame,
128 kind: BlockKind,
129) -> impl Iterator<Item = (u8, &'a Vec<super::interpolator::InterpolatedRow>)> {
130 game.blocks.iter().filter_map(move |(key, rows)| {
131 let schema = snapshot_cfg.keys.get(key)?;
132
133 (schema.kind == kind).then_some((schema.id, rows))
134 })
135}
136
137pub struct ClientState<G: GameClientDef> {
142 cfg: EngineClientConfig,
143 interpolator: Interpolator,
144 game: G,
145
146 my_game_id: Option<u32>,
148
149 frames_out: Vec<Value>,
151
152 hot: Vec<f32>,
154
155 divergence: Option<DivergenceTracker>,
158}
159
160impl<G: GameClientDef> ClientState<G> {
161 pub fn new(cfg: EngineClientConfig, game_cfg: &G::Config) -> Self {
162 let interpolator = Interpolator::new(&cfg.interpolation, cfg.snapshot.clone());
163 let game = G::new(game_cfg, &cfg);
164 let divergence = cfg.divergence.clone().map(DivergenceTracker::new);
165
166 Self {
167 cfg,
168 interpolator,
169 game,
170 my_game_id: None,
171 frames_out: Vec::new(),
172 hot: Vec::new(),
173 divergence,
174 }
175 }
176
177 pub fn push_frame(&mut self, data: &[u8], local_now: f64) -> bool {
181 let frame = match unpack::unpack_frame(data, &self.cfg.snapshot) {
182 Ok(frame) => frame,
183 Err(UnpackError::WrongVersion | UnpackError::Truncated) => return false,
184 };
185
186 if frame.port != self.cfg.snapshot.port {
187 return false;
188 }
189
190 self.interpolator.push(
191 FrameData {
192 snapshot: frame.snapshot,
193 camera: frame.camera,
194 },
195 frame.server_time,
196 local_now,
197 frame.seq,
198 );
199
200 if let Some(player) = frame.player {
201 self.my_game_id = Some(player.game_id as u32);
202
203 let offset = self.interpolator.offset().unwrap_or(0.0);
205
206 self.observe_divergence(&player, frame.server_time, offset, local_now);
207
208 self.game.on_server_state(
209 player.state,
210 player.centering,
211 frame.server_time,
212 offset,
213 local_now,
214 );
215 }
216
217 true
218 }
219
220 pub fn my_game_id(&self) -> Option<u32> {
221 self.my_game_id
222 }
223
224 pub fn offset(&self) -> Option<f64> {
225 self.interpolator.offset()
226 }
227
228 pub fn sample(&mut self, local_now: f64) -> usize {
232 self.game.set_server_offset(self.interpolator.offset());
233
234 let result = self.interpolator.sample(local_now);
235
236 for frame in result.frames {
238 self.game.track_frame(self.my_game_id, &frame);
239
240 let mut game = unpack::snapshot_to_json(&frame.snapshot);
241
242 self.game
243 .filter_frame_game(&mut game, self.my_game_id, local_now);
244
245 self.frames_out.push(json!({
246 "game": game,
247 "camera": unpack::camera_to_json(frame.camera.as_ref()),
248 }));
249
250 self.game.update_world(&frame.snapshot);
251 }
252
253 if let Some(game) = &result.game {
254 self.game.update_world_interpolated(game);
255 }
256
257 self.game.update(local_now);
258
259 let overlay = self.game.render_overlay(self.my_game_id);
260
261 self.write_hot(result.game.as_ref(), result.camera, overlay.as_ref());
262 self.hot.len()
263 }
264
265 pub fn hot(&self) -> &[f32] {
266 &self.hot
267 }
268
269 pub fn take_frames(&mut self) -> String {
271 let frames = std::mem::take(&mut self.frames_out);
272
273 serde_json::to_string(&frames).unwrap_or_else(|_| "[]".to_string())
274 }
275
276 pub fn apply_input(&mut self, action: &str, key_name: &str, local_now: f64) {
277 self.game.apply_input(action, key_name, local_now);
278 }
279
280 pub fn try_action(&mut self, local_now: f64) -> Option<String> {
281 self.game.try_action(self.my_game_id, local_now)
282 }
283
284 pub fn cycle_item(&mut self, back: bool) {
285 self.game.cycle_item(back);
286 }
287
288 pub fn set_model(&mut self, model_name: &str) {
289 self.game.set_model(model_name);
290 }
291
292 pub fn set_active(&mut self, active: bool) {
294 self.game.set_active(active);
295 }
296
297 pub fn set_map(&mut self, map_json: &str) -> Result<(), String> {
299 self.interpolator.reset();
300 self.frames_out.clear();
301 self.game.set_map(map_json)
302 }
303
304 pub fn sync_panel(&mut self, panel_json: &str) {
305 let Ok(Value::Array(items)) = serde_json::from_str(panel_json) else {
306 return;
307 };
308
309 let items: Vec<String> = items
310 .iter()
311 .map(|item| match item {
312 Value::String(s) => s.clone(),
313 other => other.to_string(),
314 })
315 .collect();
316
317 self.game.sync_panel(&items);
318 }
319
320 pub fn reset(&mut self) {
322 self.interpolator.reset();
323 self.game.reset();
324 self.frames_out.clear();
325 }
326
327 pub fn debug_json(&self) -> String {
331 json!({
332 "myGameId": self.my_game_id,
333 "offset": self.interpolator.offset(),
334 "interpolator": self.interpolator.debug_json(),
335 "hotLen": self.hot.len(),
336 "framesOut": self.frames_out.len(),
337 })
338 .to_string()
339 }
340
341 pub fn take_divergence(&mut self) -> String {
345 match &mut self.divergence {
346 Some(tracker) => tracker.take_json(),
347 None => "null".to_string(),
348 }
349 }
350
351 fn observe_divergence(
354 &mut self,
355 player: &unpack::DecodedPlayer,
356 server_time: f64,
357 offset: f64,
358 local_now: f64,
359 ) {
360 let Some(tracker) = &mut self.divergence else {
361 return;
362 };
363
364 let (source, predicted) = match self.game.predicted_state() {
366 Some(state) => (Source::State, state.to_vec()),
367 None => match self.game.render_overlay(self.my_game_id) {
368 Some(overlay) => (Source::Camera, overlay.camera.to_vec()),
369 None => return,
370 },
371 };
372
373 tracker.observe(Observation {
374 source,
375 predicted: &predicted,
376 authoritative: &player.state,
377 server_time,
378 local_now,
379 offset,
380 input_seq: player.input_seq,
381 replayed: self.game.replayed_inputs(),
382 });
383 }
384
385 pub fn decode_frame(&self, data: &[u8]) -> String {
387 match unpack::unpack_frame(data, &self.cfg.snapshot) {
388 Ok(frame) => unpack::frame_to_json(&frame).to_string(),
389 Err(_) => "null".to_string(),
390 }
391 }
392
393 fn write_hot(
398 &mut self,
399 game: Option<&InterpolatedGame>,
400 camera: Option<[f32; 2]>,
401 overlay: Option<&RenderOverlay>,
402 ) {
403 self.hot.clear();
404
405 let mut flags = 0u32;
406
407 if game.is_some() {
408 flags |= super::HOT_HAS_GAME;
409 }
410
411 if !self.frames_out.is_empty() {
412 flags |= super::HOT_HAS_FRAMES;
413 }
414
415 if overlay.is_some() {
416 flags |= super::HOT_HAS_PREDICTED;
417 }
418
419 let camera = overlay.map(|o| o.camera).or(camera);
421
422 if camera.is_some() {
423 flags |= super::HOT_HAS_CAMERA;
424 }
425
426 self.hot.push(flags as f32);
427
428 let camera = camera.unwrap_or([0.0, 0.0]);
429
430 self.hot.push(camera[0]);
431 self.hot.push(camera[1]);
432
433 let empty = InterpolatedGame::default();
434 let game = game.unwrap_or(&empty);
435
436 let tank_count: usize = blocks_of_kind(&self.cfg.snapshot, game, BlockKind::Indexed8)
441 .map(|(_, rows)| rows.len())
442 .sum();
443
444 self.hot.push(tank_count as f32);
445
446 for (key_id, rows) in blocks_of_kind(&self.cfg.snapshot, game, BlockKind::Indexed8) {
447 for row in rows {
448 self.hot.push(key_id as f32);
449 self.hot.push(row.id as f32);
450
451 for field in &row.fields {
452 self.hot.push(field_as_f32(*field));
453 }
454 }
455 }
456
457 let dynamic_count: usize =
458 blocks_of_kind(&self.cfg.snapshot, game, BlockKind::IndexedNoNull8)
459 .map(|(_, rows)| rows.len())
460 .sum();
461
462 self.hot.push(dynamic_count as f32);
463
464 for (key_id, rows) in blocks_of_kind(&self.cfg.snapshot, game, BlockKind::IndexedNoNull8) {
465 for row in rows {
466 self.hot.push(key_id as f32);
467 self.hot.push(row.id as f32);
468
469 for field in &row.fields {
470 self.hot.push(field_as_f32(*field));
471 }
472 }
473 }
474
475 if let Some(overlay) = overlay {
476 self.hot.extend_from_slice(&overlay.tail);
477 }
478 }
479}
480
481#[cfg(test)]
487mod fixture {
488 use super::*;
489 use serde::Deserialize;
490
491 #[derive(Deserialize)]
492 pub struct TestConfig {}
493
494 pub struct TestClient {
495 x: f32,
496 y: f32,
497 vx: f32,
498 vy: f32,
499 active: bool,
500 alive: bool,
501 last_update: Option<f64>,
502 report_state: bool,
505 }
506
507 impl GameClientDef for TestClient {
508 type Config = TestConfig;
509
510 fn new(_cfg: &Self::Config, _engine_cfg: &EngineClientConfig) -> Self {
511 Self {
512 x: 0.0,
513 y: 0.0,
514 vx: 0.0,
515 vy: 0.0,
516 active: false,
517 alive: true,
518 last_update: None,
519 report_state: false,
520 }
521 }
522
523 fn on_server_state(
524 &mut self,
525 state: [f32; PLAYER_STATE_LEN],
526 _centering: bool,
527 _server_time: f64,
528 _offset: f64,
529 _local_now: f64,
530 ) {
531 self.x = state[0];
532 self.y = state[1];
533 self.vx = state[3];
534 self.vy = state[4];
535 }
536
537 fn set_server_offset(&mut self, _offset: Option<f64>) {}
538
539 fn update(&mut self, local_now: f64) {
540 let dt = self
541 .last_update
542 .map(|last| (local_now - last) / 1000.0)
543 .unwrap_or(0.0) as f32;
544
545 self.x += self.vx * dt;
546 self.y += self.vy * dt;
547 self.last_update = Some(local_now);
548 }
549
550 fn track_frame(&mut self, _my_game_id: Option<u32>, _frame: &FrameData) {}
551
552 fn filter_frame_game(
553 &mut self,
554 _game: &mut Map<String, Value>,
555 _my_game_id: Option<u32>,
556 _local_now: f64,
557 ) {
558 }
559
560 fn update_world(&mut self, _snapshot: &DecodedSnapshot) {}
561
562 fn update_world_interpolated(&mut self, _game: &InterpolatedGame) {}
563
564 fn render_overlay(&self, my_game_id: Option<u32>) -> Option<RenderOverlay> {
565 let game_id = my_game_id?;
566
567 (self.active && self.alive).then(|| RenderOverlay {
568 camera: [self.x, self.y],
569 tail: vec![0.0, game_id as f32, self.x, self.y],
570 })
571 }
572
573 fn predicted_state(&self) -> Option<[f32; PLAYER_STATE_LEN]> {
574 self.report_state
575 .then_some([self.x, self.y, 0.0, self.vx, self.vy, 0.0, 0.0, 0.0])
576 }
577
578 fn replayed_inputs(&self) -> Option<(f64, f64, usize)> {
579 self.last_update.map(|last| (last - 50.0, last, 2))
580 }
581
582 fn apply_input(&mut self, _action: &str, _key_name: &str, _local_now: f64) {}
583
584 fn set_model(&mut self, model_name: &str) {
585 self.report_state = model_name == "predicted";
586 }
587
588 fn set_active(&mut self, active: bool) {
589 self.active = active;
590 }
591
592 fn set_map(&mut self, _map_json: &str) -> Result<(), String> {
593 Ok(())
594 }
595
596 fn sync_panel(&mut self, _items: &[String]) {}
597
598 fn reset(&mut self) {
599 self.x = 0.0;
600 self.y = 0.0;
601 self.last_update = None;
602 }
603
604 fn cycle_item(&mut self, _back: bool) {}
605
606 fn try_action(&mut self, _my_game_id: Option<u32>, _local_now: f64) -> Option<String> {
607 None
608 }
609 }
610}
611
612#[cfg(test)]
613mod tests {
614 use super::fixture::{TestClient, TestConfig};
615 use super::*;
616 use crate::client::{HOT_HAS_CAMERA, HOT_HAS_FRAMES, HOT_HAS_GAME, HOT_HAS_PREDICTED};
617 use crate::snapshot::{Block, CameraData, PlayerBlock, SnapshotPacker};
618
619 fn config_json() -> serde_json::Value {
620 serde_json::json!({
621 "timeStepMs": 1000.0 / 120.0,
622 "snapshot": {
623 "version": 3,
624 "port": 5,
625 "keys": {
626 "actor": { "id": 1, "kind": "indexed8", "class": "hot", "fields": [
627 { "name": "x", "ty": "f32", "interp": "lerp" },
628 { "name": "y", "ty": "f32", "interp": "lerp" }
629 ] }
630 }
631 },
632 "interpolation": { "delay": 100, "maxFrameAge": 1000 }
633 })
634 }
635
636 fn engine_client_config() -> EngineClientConfig {
637 serde_json::from_value(config_json()).unwrap()
638 }
639
640 fn config_with_divergence(capacity: usize) -> EngineClientConfig {
643 let mut json = config_json();
644
645 json["divergence"] = serde_json::json!({
646 "defaultThreshold": 1.0,
647 "capacity": capacity,
648 });
649
650 serde_json::from_value(json).unwrap()
651 }
652
653 fn make_state() -> ClientState<TestClient> {
654 ClientState::new(engine_client_config(), &TestConfig {})
655 }
656
657 fn frame_bytes(server_time: f64, seq: u32, x: f32, with_player: bool) -> Vec<u8> {
658 let cfg = engine_client_config();
659 let mut packer = SnapshotPacker::new(cfg.snapshot.clone());
660
661 packer
662 .pack_body(&[(
663 "actor".to_string(),
664 Block::Indexed8(vec![(2, Some(vec![FieldValue::F32(x), FieldValue::F32(0.0)]))]),
665 )])
666 .unwrap();
667
668 let camera = CameraData {
669 x,
670 y: 0.0,
671 force_reset: false,
672 shake: None,
673 };
674 let player = PlayerBlock {
675 game_id: 2,
676 input_seq: 0,
677 state: [x, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
678 centering: false,
679 };
680
681 packer
682 .pack_frame(
683 server_time,
684 seq,
685 Some(&camera),
686 with_player.then_some(&player),
687 )
688 .to_vec()
689 }
690
691 #[test]
692 fn push_frame_and_sample_writes_hot_layout() {
693 let mut state = make_state();
694
695 state.push_frame(&frame_bytes(1000.0, 1, 10.0, false), 1000.0);
696 state.push_frame(&frame_bytes(1100.0, 2, 20.0, false), 1100.0);
697
698 let len = state.sample(1150.0);
700 let hot = state.hot().to_vec();
701
702 assert_eq!(len, hot.len());
703
704 let flags = hot[0] as u32;
705
706 assert!(flags & HOT_HAS_GAME != 0);
707 assert!(flags & HOT_HAS_CAMERA != 0);
708 assert!(flags & HOT_HAS_FRAMES != 0);
709 assert!(flags & HOT_HAS_PREDICTED == 0);
710
711 assert_eq!(hot[3], 1.0);
713 assert_eq!(hot[4], 1.0);
714 assert_eq!(hot[5], 2.0);
715 assert_eq!(hot[6], 15.0);
716
717 let frames: Vec<serde_json::Value> =
718 serde_json::from_str(&state.take_frames()).unwrap();
719
720 assert_eq!(frames.len(), 1);
721 assert_eq!(state.take_frames(), "[]");
722 }
723
724 #[test]
725 fn render_overlay_appends_opaque_tail_and_sets_flag() {
726 let mut state = make_state();
727
728 state.set_active(true);
729 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
730
731 assert_eq!(state.my_game_id(), Some(2));
732
733 state.sample(1150.0);
734
735 let hot = state.hot().to_vec();
736 let flags = hot[0] as u32;
737
738 assert!(flags & HOT_HAS_PREDICTED != 0);
739
740 let tail = &hot[hot.len() - 4..];
742
743 assert_eq!(tail[1], 2.0); assert_eq!(hot[1], tail[2]); }
746
747 #[test]
748 fn reset_clears_predictor_and_frame_queue() {
749 let mut state = make_state();
750
751 state.set_active(true);
752 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
753 state.sample(1150.0);
754
755 state.reset();
756
757 assert_eq!(state.take_frames(), "[]");
758 }
759
760 #[test]
761 fn debug_json_reports_buffer_seq_window_and_offset() {
762 let mut state = make_state();
763
764 let empty: serde_json::Value = serde_json::from_str(&state.debug_json()).unwrap();
765
766 assert!(empty["myGameId"].is_null());
767 assert_eq!(empty["interpolator"]["buffered"], 0);
768 assert!(empty["interpolator"]["lastFrame"].is_null());
769
770 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
771 state.push_frame(&frame_bytes(1100.0, 2, 20.0, false), 1100.0);
772 state.sample(1150.0);
773
774 let dump: serde_json::Value = serde_json::from_str(&state.debug_json()).unwrap();
775
776 assert_eq!(dump["myGameId"], 2);
777 assert_eq!(dump["interpolator"]["buffered"], 2);
778 assert_eq!(dump["interpolator"]["seqWindow"], serde_json::json!([1, 2]));
779 assert_eq!(dump["interpolator"]["lastFrame"]["seq"], 2);
780 assert_eq!(dump["interpolator"]["lastFrame"]["serverTime"], 1100.0);
781 assert_eq!(dump["interpolator"]["delay"], 100.0);
782 assert_eq!(dump["offset"], dump["interpolator"]["offset"]);
783 assert_eq!(dump["hotLen"], state.hot().len());
784 assert_eq!(dump["framesOut"], 1);
785 }
786
787 #[test]
790 fn divergence_is_off_without_config() {
791 let mut state = make_state();
792
793 state.set_active(true);
794 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
795
796 assert_eq!(state.take_divergence(), "null");
797 }
798
799 #[test]
802 fn divergence_falls_back_to_overlay_camera() {
803 let mut state = ClientState::<TestClient>::new(config_with_divergence(8), &TestConfig {});
804
805 state.set_active(true);
806 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
807
808 let dump: serde_json::Value = serde_json::from_str(&state.take_divergence()).unwrap();
809 let record = &dump["records"][0];
810
811 assert_eq!(dump["samples"], 1);
812 assert_eq!(dump["violations"], 1);
813 assert_eq!(record["source"], "camera");
814 assert_eq!(record["predicted"], serde_json::json!([0.0, 0.0]));
815 assert_eq!(record["authoritative"], serde_json::json!([10.0, 0.0]));
816 assert_eq!(record["delta"][0], -10.0);
817 assert_eq!(record["exceeded"], serde_json::json!([0]));
818 assert_eq!(record["thresholds"][0], 1.0);
819 assert!(record["replayed"].is_null());
820 }
821
822 #[test]
826 fn divergence_reports_predicted_state_and_replay_window() {
827 let mut state = ClientState::<TestClient>::new(config_with_divergence(8), &TestConfig {});
828
829 state.set_active(true);
830 state.set_model("predicted");
831
832 state.push_frame(&frame_bytes(1000.0, 1, 0.0, true), 1000.0);
834 state.sample(1100.0);
835 state.push_frame(&frame_bytes(1100.0, 2, 50.0, true), 1100.0);
836
837 let dump: serde_json::Value = serde_json::from_str(&state.take_divergence()).unwrap();
838
839 assert_eq!(dump["samples"], 2);
840 assert_eq!(dump["violations"], 1);
841 assert_eq!(dump["records"].as_array().unwrap().len(), 1);
842 assert_eq!(dump["maxDelta"][0], 50.0);
843
844 let record = &dump["records"][0];
845
846 assert_eq!(record["source"], "state");
847 assert_eq!(record["serverTime"], 1100.0);
848 assert_eq!(record["localNow"], 1100.0);
849 assert_eq!(record["inputSeq"], 0);
850 assert_eq!(record["delta"][0], -50.0);
851 assert_eq!(record["exceeded"], serde_json::json!([0]));
852 assert_eq!(record["replayed"]["from"], 1050.0);
853 assert_eq!(record["replayed"]["to"], 1100.0);
854 assert_eq!(record["replayed"]["count"], 2);
855
856 let drained: serde_json::Value = serde_json::from_str(&state.take_divergence()).unwrap();
858
859 assert_eq!(drained["records"].as_array().unwrap().len(), 0);
860 assert_eq!(drained["samples"], 2);
861 assert_eq!(drained["maxDelta"][0], 50.0);
862 }
863
864 #[test]
865 fn divergence_ring_buffer_evicts_oldest_records() {
866 let mut state = ClientState::<TestClient>::new(config_with_divergence(1), &TestConfig {});
867
868 state.set_active(true);
869 state.set_model("predicted");
870 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
871 state.push_frame(&frame_bytes(1100.0, 2, 90.0, true), 1100.0);
872
873 let dump: serde_json::Value = serde_json::from_str(&state.take_divergence()).unwrap();
874
875 assert_eq!(dump["violations"], 2);
876 assert_eq!(dump["dropped"], 1);
877 assert_eq!(dump["records"].as_array().unwrap().len(), 1);
878 assert_eq!(dump["records"][0]["serverTime"], 1100.0);
879 }
880
881 #[test]
887 fn second_schema_key_of_different_block_kind_flows_into_hot_buffer() {
888 let config = serde_json::json!({
889 "timeStepMs": 1000.0 / 120.0,
890 "snapshot": {
891 "version": 3,
892 "port": 5,
893 "keys": {
894 "actor": { "id": 1, "kind": "indexed8", "class": "hot", "fields": [
895 { "name": "x", "ty": "f32", "interp": "lerp" },
896 { "name": "y", "ty": "f32", "interp": "lerp" }
897 ] },
898 "zone": { "id": 2, "kind": "indexedNoNull8", "class": "hot", "fields": [
899 { "name": "level", "ty": "f32", "interp": "discrete" }
900 ] }
901 }
902 },
903 "interpolation": { "delay": 100, "maxFrameAge": 1000 }
904 });
905 let cfg: EngineClientConfig = serde_json::from_value(config).unwrap();
906 let mut state = ClientState::<TestClient>::new(cfg.clone(), &TestConfig {});
907 let mut packer = SnapshotPacker::new(cfg.snapshot.clone());
908
909 packer
910 .pack_body(&[
911 (
912 "actor".to_string(),
913 Block::Indexed8(vec![(2, Some(vec![FieldValue::F32(10.0), FieldValue::F32(0.0)]))]),
914 ),
915 (
916 "zone".to_string(),
917 Block::IndexedNoNull8(vec![(0, vec![FieldValue::F32(7.0)])]),
918 ),
919 ])
920 .unwrap();
921
922 let frame = packer.pack_frame(1000.0, 1, None, None).to_vec();
923
924 state.push_frame(&frame, 1000.0);
925 state.push_frame(&frame, 1100.0);
926 state.sample(1150.0);
927
928 let hot = state.hot().to_vec();
929
930 assert_eq!(hot[3], 1.0); assert_eq!(hot[8], 1.0); assert_eq!(hot[9], 2.0); assert_eq!(hot[11], 7.0); }
936}