1use indexmap::IndexMap;
7use serde_json::{Map, Value, json};
8
9use crate::config::{BlockKind, BlockSchema, FieldType, FieldValue, PLAYER_STATE_LEN, SnapshotConfig};
10use crate::physics::round2;
11
12const CAMERA_FLAG_HAS_CAMERA: u8 = 1;
13const CAMERA_FLAG_FORCE_RESET: u8 = 2;
14const CAMERA_FLAG_HAS_SHAKE: u8 = 4;
15const CAMERA_FLAG_HAS_PLAYER: u8 = 8;
16
17#[derive(Clone)]
19pub struct DecodedCamera {
20 pub x: f32,
21 pub y: f32,
22 pub force_reset: bool,
23 pub shake: Option<String>,
24}
25
26#[derive(Clone)]
28pub struct DecodedPlayer {
29 pub game_id: u8,
30 pub input_seq: u32,
31 pub state: [f32; PLAYER_STATE_LEN],
32 pub centering: bool,
33}
34
35#[derive(Clone)]
39pub enum BlockData {
40 Indexed8(IndexMap<u8, Option<Vec<FieldValue>>>),
42 Indexed32(IndexMap<u32, Option<Vec<FieldValue>>>),
44 List16(Vec<Vec<FieldValue>>),
45 IndexedNoNull8(IndexMap<u8, Vec<FieldValue>>),
47}
48
49#[derive(Clone)]
51pub struct DecodedBlock {
52 pub key: String,
53 pub key_id: u8,
54 pub data: BlockData,
55}
56
57#[derive(Clone, Default)]
59pub struct DecodedSnapshot {
60 pub blocks: Vec<DecodedBlock>,
61}
62
63impl DecodedSnapshot {
64 pub fn block_by_key(&self, key: &str) -> Option<&BlockData> {
65 self.blocks
66 .iter()
67 .find(|block| block.key == key)
68 .map(|block| &block.data)
69 }
70}
71
72pub struct DecodedFrame {
74 pub port: u8,
75 pub seq: u32,
76 pub server_time: f64,
77 pub camera: Option<DecodedCamera>,
78 pub player: Option<DecodedPlayer>,
79 pub snapshot: DecodedSnapshot,
80}
81
82pub enum UnpackError {
83 WrongVersion,
85 Truncated,
87}
88
89struct Reader<'a> {
91 data: &'a [u8],
92 offset: usize,
93}
94
95impl<'a> Reader<'a> {
96 fn new(data: &'a [u8]) -> Self {
97 Self { data, offset: 0 }
98 }
99
100 fn remaining(&self) -> usize {
101 self.data.len() - self.offset
102 }
103
104 fn take(&mut self, len: usize) -> Result<&'a [u8], UnpackError> {
105 if self.remaining() < len {
106 return Err(UnpackError::Truncated);
107 }
108
109 let slice = &self.data[self.offset..self.offset + len];
110
111 self.offset += len;
112 Ok(slice)
113 }
114
115 fn u8(&mut self) -> Result<u8, UnpackError> {
116 Ok(self.take(1)?[0])
117 }
118
119 fn u16(&mut self) -> Result<u16, UnpackError> {
120 Ok(u16::from_be_bytes(self.take(2)?.try_into().unwrap()))
121 }
122
123 fn u32(&mut self) -> Result<u32, UnpackError> {
124 Ok(u32::from_be_bytes(self.take(4)?.try_into().unwrap()))
125 }
126
127 fn f32_raw(&mut self) -> Result<f32, UnpackError> {
128 Ok(f32::from_be_bytes(self.take(4)?.try_into().unwrap()))
129 }
130
131 fn f32_round2(&mut self) -> Result<f32, UnpackError> {
133 Ok(round2(self.f32_raw()?))
134 }
135
136 fn f64(&mut self) -> Result<f64, UnpackError> {
137 Ok(f64::from_be_bytes(self.take(8)?.try_into().unwrap()))
138 }
139}
140
141fn read_field(r: &mut Reader, ty: FieldType) -> Result<FieldValue, UnpackError> {
145 Ok(match ty {
146 FieldType::F32 => FieldValue::F32(r.f32_round2()?),
147 FieldType::U8 => FieldValue::U8(r.u8()?),
148 FieldType::U16 => FieldValue::U16(r.u16()?),
149 FieldType::U32 => FieldValue::U32(r.u32()?),
150 })
151}
152
153pub fn unpack_frame(data: &[u8], cfg: &SnapshotConfig) -> Result<DecodedFrame, UnpackError> {
155 let mut r = Reader::new(data);
156
157 let port = r.u8()?;
158 let version = r.u8()?;
159
160 if version != cfg.version {
161 return Err(UnpackError::WrongVersion);
162 }
163
164 let seq = r.u32()?;
165 let server_time = r.f64()?;
166 let flags = r.u8()?;
167
168 let mut camera = None;
169
170 if flags & CAMERA_FLAG_HAS_CAMERA != 0 {
171 let x = r.f32_round2()?;
172 let y = r.f32_round2()?;
173 let force_reset = flags & CAMERA_FLAG_FORCE_RESET != 0;
174
175 let shake = if flags & CAMERA_FLAG_HAS_SHAKE != 0 {
176 let len = r.u8()? as usize;
177 let bytes = r.take(len)?;
178
179 Some(String::from_utf8_lossy(bytes).into_owned())
180 } else {
181 None
182 };
183
184 camera = Some(DecodedCamera {
185 x,
186 y,
187 force_reset,
188 shake,
189 });
190 }
191
192 let mut player = None;
193
194 if flags & CAMERA_FLAG_HAS_PLAYER != 0 {
195 let game_id = r.u8()?;
196 let input_seq = r.u32()?;
197 let mut state = [0.0f32; PLAYER_STATE_LEN];
198
199 for value in &mut state {
200 *value = r.f32_raw()?; }
202
203 let centering = r.u8()? == 1;
204
205 player = Some(DecodedPlayer {
206 game_id,
207 input_seq,
208 state,
209 centering,
210 });
211 }
212
213 let mut snapshot = DecodedSnapshot::default();
214
215 while r.remaining() > 0 {
216 let key_id = r.u8()?;
217
218 let Some((key, info)) = cfg.keys.iter().find(|(_, info)| info.id == key_id) else {
220 break;
221 };
222
223 let data = match info.kind {
224 BlockKind::Indexed8 => read_indexed8(&mut r, info)?,
225 BlockKind::Indexed32 => read_indexed32(&mut r, info)?,
226 BlockKind::List16 => read_list16(&mut r, info)?,
227 BlockKind::IndexedNoNull8 => read_indexed_no_null8(&mut r, info)?,
228 };
229
230 snapshot.blocks.push(DecodedBlock {
231 key: key.clone(),
232 key_id,
233 data,
234 });
235 }
236
237 Ok(DecodedFrame {
238 port,
239 seq,
240 server_time,
241 camera,
242 player,
243 snapshot,
244 })
245}
246
247fn read_row(r: &mut Reader, schema: &BlockSchema) -> Result<Vec<FieldValue>, UnpackError> {
250 schema
251 .fields
252 .iter()
253 .map(|field| read_field(r, field.ty))
254 .collect()
255}
256
257fn read_indexed8(r: &mut Reader, schema: &BlockSchema) -> Result<BlockData, UnpackError> {
258 let count = r.u8()?;
259 let mut result = IndexMap::new();
260
261 for _ in 0..count {
262 let id = r.u8()?;
263
264 if r.u8()? == 0 {
265 result.insert(id, None);
266 continue;
267 }
268
269 result.insert(id, Some(read_row(r, schema)?));
270 }
271
272 Ok(BlockData::Indexed8(result))
273}
274
275fn read_indexed32(r: &mut Reader, schema: &BlockSchema) -> Result<BlockData, UnpackError> {
276 let count = r.u16()?;
277 let mut result = IndexMap::new();
278
279 for _ in 0..count {
280 let id = r.u32()?;
281
282 if r.u8()? == 0 {
283 result.insert(id, None);
284 continue;
285 }
286
287 result.insert(id, Some(read_row(r, schema)?));
288 }
289
290 Ok(BlockData::Indexed32(result))
291}
292
293fn read_list16(r: &mut Reader, schema: &BlockSchema) -> Result<BlockData, UnpackError> {
294 let count = r.u16()?;
295 let mut result = Vec::with_capacity(count as usize);
296
297 for _ in 0..count {
298 result.push(read_row(r, schema)?);
299 }
300
301 Ok(BlockData::List16(result))
302}
303
304fn read_indexed_no_null8(r: &mut Reader, schema: &BlockSchema) -> Result<BlockData, UnpackError> {
305 let count = r.u8()?;
306 let mut result = IndexMap::new();
307
308 for _ in 0..count {
309 let index = r.u8()?;
310
311 result.insert(index, read_row(r, schema)?);
312 }
313
314 Ok(BlockData::IndexedNoNull8(result))
315}
316
317pub fn round2_f64(v: f32) -> f64 {
322 ((v as f64) * 100.0).round() / 100.0
323}
324
325pub fn to_base36(mut v: u32) -> String {
327 const DIGITS: &[u8; 36] = b"0123456789abcdefghijklmnopqrstuvwxyz";
328
329 if v == 0 {
330 return "0".to_string();
331 }
332
333 let mut out = Vec::new();
334
335 while v > 0 {
336 out.push(DIGITS[(v % 36) as usize]);
337 v /= 36;
338 }
339
340 out.reverse();
341 String::from_utf8(out).unwrap()
342}
343
344pub fn camera_to_json(camera: Option<&DecodedCamera>) -> Value {
346 let Some(camera) = camera else {
347 return json!(0);
348 };
349
350 let mut arr = vec![
351 Value::from(round2_f64(camera.x)),
352 Value::from(round2_f64(camera.y)),
353 ];
354
355 if camera.force_reset {
356 arr.push(Value::from(true));
357 }
358
359 if let Some(shake) = &camera.shake {
360 while arr.len() < 3 {
362 arr.push(Value::Null);
363 }
364
365 arr.push(Value::from(shake.clone()));
366 }
367
368 Value::Array(arr)
369}
370
371fn field_value_to_json(value: FieldValue) -> Value {
372 match value {
373 FieldValue::F32(v) => Value::from(round2_f64(v)),
374 FieldValue::U8(v) => Value::from(v),
375 FieldValue::U16(v) => Value::from(v),
376 FieldValue::U32(v) => Value::from(v),
377 }
378}
379
380fn row_to_json(fields: &[FieldValue]) -> Value {
383 Value::Array(fields.iter().copied().map(field_value_to_json).collect())
384}
385
386pub fn snapshot_to_json(snapshot: &DecodedSnapshot) -> Map<String, Value> {
392 let mut result = Map::new();
393
394 for block in &snapshot.blocks {
395 let value = match &block.data {
396 BlockData::Indexed8(items) => {
397 let mut map = Map::new();
398
399 for (id, row) in items {
400 map.insert(
401 id.to_string(),
402 row.as_deref().map_or(Value::Null, row_to_json),
403 );
404 }
405
406 Value::Object(map)
407 }
408 BlockData::Indexed32(items) => {
409 let mut map = Map::new();
410
411 for (id, row) in items {
412 map.insert(
413 to_base36(*id),
414 row.as_deref().map_or(Value::Null, row_to_json),
415 );
416 }
417
418 Value::Object(map)
419 }
420 BlockData::List16(items) => {
421 Value::Array(items.iter().map(|fields| row_to_json(fields)).collect())
422 }
423 BlockData::IndexedNoNull8(items) => {
424 let mut map = Map::new();
425
426 for (index, fields) in items {
427 map.insert(format!("d{index}"), row_to_json(fields));
428 }
429
430 Value::Object(map)
431 }
432 };
433
434 result.insert(block.key.clone(), value);
435 }
436
437 result
438}
439
440pub fn frame_to_json(frame: &DecodedFrame) -> Value {
443 let player = frame.player.as_ref().map_or(Value::Null, |p| {
444 json!({
445 "gameId": p.game_id,
446 "inputSeq": p.input_seq,
447 "state": p.state.iter().map(|v| *v as f64).collect::<Vec<f64>>(),
449 "centering": p.centering,
450 })
451 });
452
453 json!({
454 "port": frame.port,
455 "seq": frame.seq,
456 "serverTime": frame.server_time,
457 "camera": camera_to_json(frame.camera.as_ref()),
458 "player": player,
459 "snapshot": Value::Object(snapshot_to_json(&frame.snapshot)),
460 })
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466 use crate::config::test_support::full_snapshot_config;
467 use crate::snapshot::{Block, CameraData, PlayerBlock, SnapshotPacker};
468
469 fn test_config() -> SnapshotConfig {
472 full_snapshot_config(3, 5)
473 }
474
475 fn packed_frame(
476 blocks: &[(String, Block)],
477 camera: Option<&CameraData>,
478 player: Option<&PlayerBlock>,
479 ) -> Vec<u8> {
480 let mut packer = SnapshotPacker::new(test_config());
481
482 packer.pack_body(blocks).unwrap();
483 packer.pack_frame(1234.5, 42, camera, player).to_vec()
484 }
485
486 fn tank_row(floats: [f32; 7], condition: u8, size: u8, team: u8) -> Vec<FieldValue> {
487 let mut fields: Vec<FieldValue> = floats.iter().copied().map(FieldValue::F32).collect();
488
489 fields.push(FieldValue::U8(condition));
490 fields.push(FieldValue::U8(size));
491 fields.push(FieldValue::U8(team));
492 fields
493 }
494
495 fn tracer_row(floats: [f32; 6], was_hit: bool, shooter: u8) -> Vec<FieldValue> {
496 let mut fields: Vec<FieldValue> = floats.iter().copied().map(FieldValue::F32).collect();
497
498 fields.push(FieldValue::U8(was_hit as u8));
499 fields.push(FieldValue::U8(shooter));
500 fields
501 }
502
503 fn bomb_row(x: f32, y: f32, angle: f32, size: u8, time: u16, owner: u8) -> Vec<FieldValue> {
504 vec![
505 FieldValue::F32(x),
506 FieldValue::F32(y),
507 FieldValue::F32(angle),
508 FieldValue::U8(size),
509 FieldValue::U16(time),
510 FieldValue::U8(owner),
511 ]
512 }
513
514 fn explosion_row(x: f32, y: f32, radius: f32) -> Vec<FieldValue> {
515 vec![FieldValue::F32(x), FieldValue::F32(y), FieldValue::F32(radius)]
516 }
517
518 fn field_f32(fields: &[FieldValue], i: usize) -> f32 {
519 match fields[i] {
520 FieldValue::F32(v) => v,
521 _ => panic!("поле {i} не F32"),
522 }
523 }
524
525 #[test]
526 fn full_frame_round_trip() {
527 let blocks = vec![
528 (
529 "m1".to_string(),
530 Block::Indexed8(vec![
531 (
532 2,
533 Some(tank_row(
534 [10.567, -3.141, 1.5, 0.25, 100.0, -50.5, 1.2],
535 3,
536 2,
537 1,
538 )),
539 ),
540 (3, None),
541 ]),
542 ),
543 (
544 "w1".to_string(),
545 Block::List16(vec![tracer_row(
546 [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
547 true,
548 7,
549 )]),
550 ),
551 (
552 "w2".to_string(),
553 Block::Indexed32(vec![
554 (
555 u32::from_str_radix("a1f", 36).unwrap(),
557 Some(bomb_row(5.5, 6.5, 0.0, 8, 300, 2)),
558 ),
559 (1, None),
560 ]),
561 ),
562 (
563 "w2e".to_string(),
564 Block::List16(vec![explosion_row(100.0, 200.0, 50.0)]),
565 ),
566 (
567 "c1".to_string(),
568 Block::IndexedNoNull8(vec![(0, explosion_row(10.0, 20.0, 0.5))]),
569 ),
570 ];
571
572 let camera = CameraData {
573 x: 10.5,
574 y: -3.25,
575 force_reset: true,
576 shake: Some("20:200".to_string()),
577 };
578 let player = PlayerBlock {
579 game_id: 2,
580 input_seq: 77,
581 state: [10.56789, 2.0, 0.5, 3.0, 4.0, 0.1, 0.2, 0.7],
582 centering: true,
583 };
584
585 let data = packed_frame(&blocks, Some(&camera), Some(&player));
586 let frame = unpack_frame(&data, &test_config()).ok().unwrap();
587
588 assert_eq!(frame.port, 5);
589 assert_eq!(frame.seq, 42);
590 assert_eq!(frame.server_time, 1234.5);
591
592 let cam = frame.camera.as_ref().unwrap();
594
595 assert_eq!(cam.x, 10.5);
596 assert_eq!(cam.y, -3.25);
597 assert!(cam.force_reset);
598 assert_eq!(cam.shake.as_deref(), Some("20:200"));
599
600 let p = frame.player.as_ref().unwrap();
602
603 assert_eq!(p.game_id, 2);
604 assert_eq!(p.input_seq, 77);
605 assert_eq!(p.state[0], 10.56789f32);
606 assert!(p.centering);
607
608 let Some(BlockData::Indexed8(tanks)) = frame.snapshot.block_by_key("m1") else {
610 panic!("нет блока m1");
611 };
612 let row = tanks[&2].as_ref().unwrap();
613
614 assert_eq!(field_f32(row, 0), 10.57);
615 assert_eq!(field_f32(row, 1), -3.14);
616
617 match (row[7], row[8], row[9]) {
618 (FieldValue::U8(condition), FieldValue::U8(size), FieldValue::U8(team)) => {
619 assert_eq!((condition, size, team), (3, 2, 1));
620 }
621 _ => panic!("неожиданные типы полей tank"),
622 }
623 assert!(tanks[&3].is_none()); let Some(BlockData::List16(tracers)) = frame.snapshot.block_by_key("w1") else {
626 panic!("нет блока w1");
627 };
628
629 match tracers[0][6] {
630 FieldValue::U8(v) => assert_eq!(v, 1),
631 _ => panic!("поле 6 не U8"),
632 }
633 match tracers[0][7] {
634 FieldValue::U8(v) => assert_eq!(v, 7),
635 _ => panic!("поле 7 не U8"),
636 }
637
638 let Some(BlockData::Indexed32(bombs)) = frame.snapshot.block_by_key("w2") else {
639 panic!("нет блока w2");
640 };
641 let bomb = bombs[&u32::from_str_radix("a1f", 36).unwrap()]
642 .as_ref()
643 .unwrap();
644
645 match (bomb[3], bomb[4], bomb[5]) {
646 (FieldValue::U8(size), FieldValue::U16(time), FieldValue::U8(owner)) => {
647 assert_eq!((size, time, owner), (8, 300, 2));
648 }
649 _ => panic!("неожиданные типы полей bomb"),
650 }
651 assert!(bombs[&1].is_none());
652
653 let Some(BlockData::List16(explosions)) = frame.snapshot.block_by_key("w2e") else {
654 panic!("нет блока w2e");
655 };
656
657 assert_eq!(
658 (
659 field_f32(&explosions[0], 0),
660 field_f32(&explosions[0], 1),
661 field_f32(&explosions[0], 2)
662 ),
663 (100.0, 200.0, 50.0)
664 );
665
666 let Some(BlockData::IndexedNoNull8(dynamics)) = frame.snapshot.block_by_key("c1") else {
667 panic!("нет блока c1");
668 };
669
670 let dyn_row = &dynamics[&0];
671
672 assert_eq!(
673 (
674 field_f32(dyn_row, 0),
675 field_f32(dyn_row, 1),
676 field_f32(dyn_row, 2)
677 ),
678 (10.0, 20.0, 0.5)
679 );
680 }
681
682 #[test]
683 fn wrong_version_is_rejected() {
684 let mut data = packed_frame(&[], None, None);
685
686 data[1] = 99;
687 assert!(matches!(
688 unpack_frame(&data, &test_config()),
689 Err(UnpackError::WrongVersion)
690 ));
691 }
692
693 #[test]
694 fn truncated_frame_is_error() {
695 let data = packed_frame(
696 &[(
697 "m1".to_string(),
698 Block::Indexed8(vec![(1, Some(tank_row([0.0; 7], 3, 2, 1)))]),
699 )],
700 None,
701 None,
702 );
703
704 assert!(matches!(
705 unpack_frame(&data[..data.len() - 4], &test_config()),
706 Err(UnpackError::Truncated)
707 ));
708 }
709
710 #[test]
711 fn unknown_block_id_drops_rest() {
712 let mut data = packed_frame(
713 &[(
714 "w2e".to_string(),
715 Block::List16(vec![explosion_row(1.0, 2.0, 3.0)]),
716 )],
717 None,
718 None,
719 );
720
721 data[15] = 200;
723
724 let frame = unpack_frame(&data, &test_config()).ok().unwrap();
725
726 assert!(frame.snapshot.blocks.is_empty());
727 }
728
729 #[test]
730 fn base36_matches_js() {
731 assert_eq!(to_base36(0), "0");
732 assert_eq!(to_base36(35), "z");
733 assert_eq!(to_base36(u32::from_str_radix("a1f", 36).unwrap()), "a1f");
734 }
735
736 #[test]
737 fn camera_json_forms() {
738 assert_eq!(camera_to_json(None), json!(0));
739
740 let plain = DecodedCamera {
741 x: 1.5,
742 y: 2.5,
743 force_reset: false,
744 shake: None,
745 };
746
747 assert_eq!(camera_to_json(Some(&plain)), json!([1.5, 2.5]));
748
749 let reset = DecodedCamera {
750 force_reset: true,
751 ..plain.clone()
752 };
753
754 assert_eq!(camera_to_json(Some(&reset)), json!([1.5, 2.5, true]));
755
756 let shake = DecodedCamera {
758 shake: Some("20:200".to_string()),
759 ..plain
760 };
761
762 assert_eq!(
763 camera_to_json(Some(&shake)),
764 json!([1.5, 2.5, null, "20:200"])
765 );
766 }
767
768 #[test]
769 fn snapshot_json_matches_unpack_frame_forms() {
770 let blocks = vec![
771 (
772 "m1".to_string(),
773 Block::Indexed8(vec![
774 (
775 2,
776 Some(tank_row([51.28, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0], 3, 2, 1)),
777 ),
778 (3, None),
779 ]),
780 ),
781 (
782 "w2".to_string(),
783 Block::Indexed32(vec![(
784 u32::from_str_radix("a1f", 36).unwrap(),
785 Some(bomb_row(1.0, 2.0, 0.0, 8, 300, 2)),
786 )]),
787 ),
788 ];
789 let data = packed_frame(&blocks, None, None);
790 let frame = unpack_frame(&data, &test_config()).ok().unwrap();
791 let game = Value::Object(snapshot_to_json(&frame.snapshot));
792
793 assert_eq!(
795 game,
796 json!({
797 "m1": { "2": [51.28, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 3, 2, 1], "3": null },
798 "w2": { "a1f": [1.0, 2.0, 0.0, 8, 300, 2] },
799 })
800 );
801 }
802}