1use serde_json::{Map, Value, json};
17
18use super::interpolator::{FrameData, InterpolatedGame, Interpolator};
19use super::unpack::{self, DecodedSnapshot, UnpackError};
20use crate::config::{BlockKind, EngineClientConfig, FieldValue, PLAYER_STATE_LEN, SnapshotConfig};
21
22pub struct RenderOverlay {
25 pub camera: [f32; 2],
26 pub tail: Vec<f32>,
27}
28
29pub trait GameClientDef: Sized {
34 type Config: serde::de::DeserializeOwned;
35
36 fn new(cfg: &Self::Config, engine_cfg: &EngineClientConfig) -> Self;
37
38 fn on_server_state(
43 &mut self,
44 state: [f32; PLAYER_STATE_LEN],
45 centering: bool,
46 server_time: f64,
47 offset: f64,
48 local_now: f64,
49 );
50
51 fn set_server_offset(&mut self, offset: Option<f64>);
53
54 fn update(&mut self, local_now: f64);
56
57 fn track_frame(&mut self, my_game_id: Option<u32>, frame: &FrameData);
61
62 fn filter_frame_game(&mut self, game: &mut Map<String, Value>, my_game_id: Option<u32>, local_now: f64);
65
66 fn update_world(&mut self, snapshot: &DecodedSnapshot);
69
70 fn update_world_interpolated(&mut self, game: &InterpolatedGame);
72
73 fn render_overlay(&self, my_game_id: Option<u32>) -> Option<RenderOverlay>;
77
78 fn apply_input(&mut self, action: &str, key_name: &str, local_now: f64);
79 fn set_model(&mut self, model_name: &str);
80 fn set_active(&mut self, active: bool);
81 fn set_map(&mut self, map_json: &str) -> Result<(), String>;
82 fn sync_panel(&mut self, items: &[String]);
83 fn reset(&mut self);
84
85 fn cycle_item(&mut self, back: bool);
88
89 fn try_action(&mut self, my_game_id: Option<u32>, local_now: f64) -> Option<String>;
92}
93
94fn field_as_f32(value: FieldValue) -> f32 {
96 match value {
97 FieldValue::F32(v) => v,
98 FieldValue::U8(v) => v as f32,
99 FieldValue::U16(v) => v as f32,
100 FieldValue::U32(v) => v as f32,
101 }
102}
103
104fn blocks_of_kind<'a>(
107 snapshot_cfg: &'a SnapshotConfig,
108 game: &'a InterpolatedGame,
109 kind: BlockKind,
110) -> impl Iterator<Item = (u8, &'a Vec<super::interpolator::InterpolatedRow>)> {
111 game.blocks.iter().filter_map(move |(key, rows)| {
112 let schema = snapshot_cfg.keys.get(key)?;
113
114 (schema.kind == kind).then_some((schema.id, rows))
115 })
116}
117
118pub struct ClientState<G: GameClientDef> {
123 cfg: EngineClientConfig,
124 interpolator: Interpolator,
125 game: G,
126
127 my_game_id: Option<u32>,
129
130 frames_out: Vec<Value>,
132
133 hot: Vec<f32>,
135}
136
137impl<G: GameClientDef> ClientState<G> {
138 pub fn new(cfg: EngineClientConfig, game_cfg: &G::Config) -> Self {
139 let interpolator = Interpolator::new(&cfg.interpolation, cfg.snapshot.clone());
140 let game = G::new(game_cfg, &cfg);
141
142 Self {
143 cfg,
144 interpolator,
145 game,
146 my_game_id: None,
147 frames_out: Vec::new(),
148 hot: Vec::new(),
149 }
150 }
151
152 pub fn push_frame(&mut self, data: &[u8], local_now: f64) -> bool {
156 let frame = match unpack::unpack_frame(data, &self.cfg.snapshot) {
157 Ok(frame) => frame,
158 Err(UnpackError::WrongVersion | UnpackError::Truncated) => return false,
159 };
160
161 if frame.port != self.cfg.snapshot.port {
162 return false;
163 }
164
165 self.interpolator.push(
166 FrameData {
167 snapshot: frame.snapshot,
168 camera: frame.camera,
169 },
170 frame.server_time,
171 local_now,
172 frame.seq,
173 );
174
175 if let Some(player) = frame.player {
176 self.my_game_id = Some(player.game_id as u32);
177
178 let offset = self.interpolator.offset().unwrap_or(0.0);
180
181 self.game.on_server_state(
182 player.state,
183 player.centering,
184 frame.server_time,
185 offset,
186 local_now,
187 );
188 }
189
190 true
191 }
192
193 pub fn my_game_id(&self) -> Option<u32> {
194 self.my_game_id
195 }
196
197 pub fn offset(&self) -> Option<f64> {
198 self.interpolator.offset()
199 }
200
201 pub fn sample(&mut self, local_now: f64) -> usize {
205 self.game.set_server_offset(self.interpolator.offset());
206
207 let result = self.interpolator.sample(local_now);
208
209 for frame in result.frames {
211 self.game.track_frame(self.my_game_id, &frame);
212
213 let mut game = unpack::snapshot_to_json(&frame.snapshot);
214
215 self.game
216 .filter_frame_game(&mut game, self.my_game_id, local_now);
217
218 self.frames_out.push(json!({
219 "game": game,
220 "camera": unpack::camera_to_json(frame.camera.as_ref()),
221 }));
222
223 self.game.update_world(&frame.snapshot);
224 }
225
226 if let Some(game) = &result.game {
227 self.game.update_world_interpolated(game);
228 }
229
230 self.game.update(local_now);
231
232 let overlay = self.game.render_overlay(self.my_game_id);
233
234 self.write_hot(result.game.as_ref(), result.camera, overlay.as_ref());
235 self.hot.len()
236 }
237
238 pub fn hot(&self) -> &[f32] {
239 &self.hot
240 }
241
242 pub fn take_frames(&mut self) -> String {
244 let frames = std::mem::take(&mut self.frames_out);
245
246 serde_json::to_string(&frames).unwrap_or_else(|_| "[]".to_string())
247 }
248
249 pub fn apply_input(&mut self, action: &str, key_name: &str, local_now: f64) {
250 self.game.apply_input(action, key_name, local_now);
251 }
252
253 pub fn try_action(&mut self, local_now: f64) -> Option<String> {
254 self.game.try_action(self.my_game_id, local_now)
255 }
256
257 pub fn cycle_item(&mut self, back: bool) {
258 self.game.cycle_item(back);
259 }
260
261 pub fn set_model(&mut self, model_name: &str) {
262 self.game.set_model(model_name);
263 }
264
265 pub fn set_active(&mut self, active: bool) {
267 self.game.set_active(active);
268 }
269
270 pub fn set_map(&mut self, map_json: &str) -> Result<(), String> {
272 self.interpolator.reset();
273 self.frames_out.clear();
274 self.game.set_map(map_json)
275 }
276
277 pub fn sync_panel(&mut self, panel_json: &str) {
278 let Ok(Value::Array(items)) = serde_json::from_str(panel_json) else {
279 return;
280 };
281
282 let items: Vec<String> = items
283 .iter()
284 .map(|item| match item {
285 Value::String(s) => s.clone(),
286 other => other.to_string(),
287 })
288 .collect();
289
290 self.game.sync_panel(&items);
291 }
292
293 pub fn reset(&mut self) {
295 self.interpolator.reset();
296 self.game.reset();
297 self.frames_out.clear();
298 }
299
300 pub fn decode_frame(&self, data: &[u8]) -> String {
302 match unpack::unpack_frame(data, &self.cfg.snapshot) {
303 Ok(frame) => unpack::frame_to_json(&frame).to_string(),
304 Err(_) => "null".to_string(),
305 }
306 }
307
308 fn write_hot(
313 &mut self,
314 game: Option<&InterpolatedGame>,
315 camera: Option<[f32; 2]>,
316 overlay: Option<&RenderOverlay>,
317 ) {
318 self.hot.clear();
319
320 let mut flags = 0u32;
321
322 if game.is_some() {
323 flags |= super::HOT_HAS_GAME;
324 }
325
326 if !self.frames_out.is_empty() {
327 flags |= super::HOT_HAS_FRAMES;
328 }
329
330 if overlay.is_some() {
331 flags |= super::HOT_HAS_PREDICTED;
332 }
333
334 let camera = overlay.map(|o| o.camera).or(camera);
336
337 if camera.is_some() {
338 flags |= super::HOT_HAS_CAMERA;
339 }
340
341 self.hot.push(flags as f32);
342
343 let camera = camera.unwrap_or([0.0, 0.0]);
344
345 self.hot.push(camera[0]);
346 self.hot.push(camera[1]);
347
348 let empty = InterpolatedGame::default();
349 let game = game.unwrap_or(&empty);
350
351 let tank_count: usize = blocks_of_kind(&self.cfg.snapshot, game, BlockKind::Indexed8)
356 .map(|(_, rows)| rows.len())
357 .sum();
358
359 self.hot.push(tank_count as f32);
360
361 for (key_id, rows) in blocks_of_kind(&self.cfg.snapshot, game, BlockKind::Indexed8) {
362 for row in rows {
363 self.hot.push(key_id as f32);
364 self.hot.push(row.id as f32);
365
366 for field in &row.fields {
367 self.hot.push(field_as_f32(*field));
368 }
369 }
370 }
371
372 let dynamic_count: usize =
373 blocks_of_kind(&self.cfg.snapshot, game, BlockKind::IndexedNoNull8)
374 .map(|(_, rows)| rows.len())
375 .sum();
376
377 self.hot.push(dynamic_count as f32);
378
379 for (key_id, rows) in blocks_of_kind(&self.cfg.snapshot, game, BlockKind::IndexedNoNull8) {
380 for row in rows {
381 self.hot.push(key_id as f32);
382 self.hot.push(row.id as f32);
383
384 for field in &row.fields {
385 self.hot.push(field_as_f32(*field));
386 }
387 }
388 }
389
390 if let Some(overlay) = overlay {
391 self.hot.extend_from_slice(&overlay.tail);
392 }
393 }
394}
395
396#[cfg(test)]
402mod fixture {
403 use super::*;
404 use serde::Deserialize;
405
406 #[derive(Deserialize)]
407 pub struct TestConfig {}
408
409 pub struct TestClient {
410 x: f32,
411 y: f32,
412 vx: f32,
413 vy: f32,
414 active: bool,
415 alive: bool,
416 last_update: Option<f64>,
417 }
418
419 impl GameClientDef for TestClient {
420 type Config = TestConfig;
421
422 fn new(_cfg: &Self::Config, _engine_cfg: &EngineClientConfig) -> Self {
423 Self {
424 x: 0.0,
425 y: 0.0,
426 vx: 0.0,
427 vy: 0.0,
428 active: false,
429 alive: true,
430 last_update: None,
431 }
432 }
433
434 fn on_server_state(
435 &mut self,
436 state: [f32; PLAYER_STATE_LEN],
437 _centering: bool,
438 _server_time: f64,
439 _offset: f64,
440 _local_now: f64,
441 ) {
442 self.x = state[0];
443 self.y = state[1];
444 self.vx = state[3];
445 self.vy = state[4];
446 }
447
448 fn set_server_offset(&mut self, _offset: Option<f64>) {}
449
450 fn update(&mut self, local_now: f64) {
451 let dt = self
452 .last_update
453 .map(|last| (local_now - last) / 1000.0)
454 .unwrap_or(0.0) as f32;
455
456 self.x += self.vx * dt;
457 self.y += self.vy * dt;
458 self.last_update = Some(local_now);
459 }
460
461 fn track_frame(&mut self, _my_game_id: Option<u32>, _frame: &FrameData) {}
462
463 fn filter_frame_game(
464 &mut self,
465 _game: &mut Map<String, Value>,
466 _my_game_id: Option<u32>,
467 _local_now: f64,
468 ) {
469 }
470
471 fn update_world(&mut self, _snapshot: &DecodedSnapshot) {}
472
473 fn update_world_interpolated(&mut self, _game: &InterpolatedGame) {}
474
475 fn render_overlay(&self, my_game_id: Option<u32>) -> Option<RenderOverlay> {
476 let game_id = my_game_id?;
477
478 (self.active && self.alive).then(|| RenderOverlay {
479 camera: [self.x, self.y],
480 tail: vec![0.0, game_id as f32, self.x, self.y],
481 })
482 }
483
484 fn apply_input(&mut self, _action: &str, _key_name: &str, _local_now: f64) {}
485
486 fn set_model(&mut self, _model_name: &str) {}
487
488 fn set_active(&mut self, active: bool) {
489 self.active = active;
490 }
491
492 fn set_map(&mut self, _map_json: &str) -> Result<(), String> {
493 Ok(())
494 }
495
496 fn sync_panel(&mut self, _items: &[String]) {}
497
498 fn reset(&mut self) {
499 self.x = 0.0;
500 self.y = 0.0;
501 self.last_update = None;
502 }
503
504 fn cycle_item(&mut self, _back: bool) {}
505
506 fn try_action(&mut self, _my_game_id: Option<u32>, _local_now: f64) -> Option<String> {
507 None
508 }
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use super::fixture::{TestClient, TestConfig};
515 use super::*;
516 use crate::client::{HOT_HAS_CAMERA, HOT_HAS_FRAMES, HOT_HAS_GAME, HOT_HAS_PREDICTED};
517 use crate::snapshot::{Block, CameraData, PlayerBlock, SnapshotPacker};
518
519 fn config_json() -> serde_json::Value {
520 serde_json::json!({
521 "timeStepMs": 1000.0 / 120.0,
522 "snapshot": {
523 "version": 3,
524 "port": 5,
525 "keys": {
526 "actor": { "id": 1, "kind": "indexed8", "class": "hot", "fields": [
527 { "name": "x", "ty": "f32", "interp": "lerp" },
528 { "name": "y", "ty": "f32", "interp": "lerp" }
529 ] }
530 }
531 },
532 "interpolation": { "delay": 100, "maxFrameAge": 1000 }
533 })
534 }
535
536 fn engine_client_config() -> EngineClientConfig {
537 serde_json::from_value(config_json()).unwrap()
538 }
539
540 fn make_state() -> ClientState<TestClient> {
541 ClientState::new(engine_client_config(), &TestConfig {})
542 }
543
544 fn frame_bytes(server_time: f64, seq: u32, x: f32, with_player: bool) -> Vec<u8> {
545 let cfg = engine_client_config();
546 let mut packer = SnapshotPacker::new(cfg.snapshot.clone());
547
548 packer
549 .pack_body(&[(
550 "actor".to_string(),
551 Block::Indexed8(vec![(2, Some(vec![FieldValue::F32(x), FieldValue::F32(0.0)]))]),
552 )])
553 .unwrap();
554
555 let camera = CameraData {
556 x,
557 y: 0.0,
558 force_reset: false,
559 shake: None,
560 };
561 let player = PlayerBlock {
562 game_id: 2,
563 input_seq: 0,
564 state: [x, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
565 centering: false,
566 };
567
568 packer
569 .pack_frame(
570 server_time,
571 seq,
572 Some(&camera),
573 with_player.then_some(&player),
574 )
575 .to_vec()
576 }
577
578 #[test]
579 fn push_frame_and_sample_writes_hot_layout() {
580 let mut state = make_state();
581
582 state.push_frame(&frame_bytes(1000.0, 1, 10.0, false), 1000.0);
583 state.push_frame(&frame_bytes(1100.0, 2, 20.0, false), 1100.0);
584
585 let len = state.sample(1150.0);
587 let hot = state.hot().to_vec();
588
589 assert_eq!(len, hot.len());
590
591 let flags = hot[0] as u32;
592
593 assert!(flags & HOT_HAS_GAME != 0);
594 assert!(flags & HOT_HAS_CAMERA != 0);
595 assert!(flags & HOT_HAS_FRAMES != 0);
596 assert!(flags & HOT_HAS_PREDICTED == 0);
597
598 assert_eq!(hot[3], 1.0);
600 assert_eq!(hot[4], 1.0);
601 assert_eq!(hot[5], 2.0);
602 assert_eq!(hot[6], 15.0);
603
604 let frames: Vec<serde_json::Value> =
605 serde_json::from_str(&state.take_frames()).unwrap();
606
607 assert_eq!(frames.len(), 1);
608 assert_eq!(state.take_frames(), "[]");
609 }
610
611 #[test]
612 fn render_overlay_appends_opaque_tail_and_sets_flag() {
613 let mut state = make_state();
614
615 state.set_active(true);
616 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
617
618 assert_eq!(state.my_game_id(), Some(2));
619
620 state.sample(1150.0);
621
622 let hot = state.hot().to_vec();
623 let flags = hot[0] as u32;
624
625 assert!(flags & HOT_HAS_PREDICTED != 0);
626
627 let tail = &hot[hot.len() - 4..];
629
630 assert_eq!(tail[1], 2.0); assert_eq!(hot[1], tail[2]); }
633
634 #[test]
635 fn reset_clears_predictor_and_frame_queue() {
636 let mut state = make_state();
637
638 state.set_active(true);
639 state.push_frame(&frame_bytes(1000.0, 1, 10.0, true), 1000.0);
640 state.sample(1150.0);
641
642 state.reset();
643
644 assert_eq!(state.take_frames(), "[]");
645 }
646
647 #[test]
653 fn second_schema_key_of_different_block_kind_flows_into_hot_buffer() {
654 let config = serde_json::json!({
655 "timeStepMs": 1000.0 / 120.0,
656 "snapshot": {
657 "version": 3,
658 "port": 5,
659 "keys": {
660 "actor": { "id": 1, "kind": "indexed8", "class": "hot", "fields": [
661 { "name": "x", "ty": "f32", "interp": "lerp" },
662 { "name": "y", "ty": "f32", "interp": "lerp" }
663 ] },
664 "zone": { "id": 2, "kind": "indexedNoNull8", "class": "hot", "fields": [
665 { "name": "level", "ty": "f32", "interp": "discrete" }
666 ] }
667 }
668 },
669 "interpolation": { "delay": 100, "maxFrameAge": 1000 }
670 });
671 let cfg: EngineClientConfig = serde_json::from_value(config).unwrap();
672 let mut state = ClientState::<TestClient>::new(cfg.clone(), &TestConfig {});
673 let mut packer = SnapshotPacker::new(cfg.snapshot.clone());
674
675 packer
676 .pack_body(&[
677 (
678 "actor".to_string(),
679 Block::Indexed8(vec![(2, Some(vec![FieldValue::F32(10.0), FieldValue::F32(0.0)]))]),
680 ),
681 (
682 "zone".to_string(),
683 Block::IndexedNoNull8(vec![(0, vec![FieldValue::F32(7.0)])]),
684 ),
685 ])
686 .unwrap();
687
688 let frame = packer.pack_frame(1000.0, 1, None, None).to_vec();
689
690 state.push_frame(&frame, 1000.0);
691 state.push_frame(&frame, 1100.0);
692 state.sample(1150.0);
693
694 let hot = state.hot().to_vec();
695
696 assert_eq!(hot[3], 1.0); assert_eq!(hot[8], 1.0); assert_eq!(hot[9], 2.0); assert_eq!(hot[11], 7.0); }
702}