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 zero_field(ty: FieldType) -> FieldValue {
250 match ty {
251 FieldType::F32 => FieldValue::F32(0.0),
252 FieldType::U8 => FieldValue::U8(0),
253 FieldType::U16 => FieldValue::U16(0),
254 FieldType::U32 => FieldValue::U32(0),
255 }
256}
257
258fn read_row(r: &mut Reader, schema: &BlockSchema) -> Result<Vec<FieldValue>, UnpackError> {
265 let has_tail = match schema.optional_from {
266 None => true,
267 Some(_) => r.u8()? == 1,
268 };
269
270 let read_len = if has_tail {
271 schema.fields.len()
272 } else {
273 schema.required_len()
274 };
275
276 schema
277 .fields
278 .iter()
279 .enumerate()
280 .map(|(i, field)| {
281 if i < read_len {
282 read_field(r, field.ty)
283 } else {
284 Ok(zero_field(field.ty))
285 }
286 })
287 .collect()
288}
289
290fn read_indexed8(r: &mut Reader, schema: &BlockSchema) -> Result<BlockData, UnpackError> {
291 let count = r.u8()?;
292 let mut result = IndexMap::new();
293
294 for _ in 0..count {
295 let id = r.u8()?;
296
297 if r.u8()? == 0 {
298 result.insert(id, None);
299 continue;
300 }
301
302 result.insert(id, Some(read_row(r, schema)?));
303 }
304
305 Ok(BlockData::Indexed8(result))
306}
307
308fn read_indexed32(r: &mut Reader, schema: &BlockSchema) -> Result<BlockData, UnpackError> {
309 let count = r.u16()?;
310 let mut result = IndexMap::new();
311
312 for _ in 0..count {
313 let id = r.u32()?;
314
315 if r.u8()? == 0 {
316 result.insert(id, None);
317 continue;
318 }
319
320 result.insert(id, Some(read_row(r, schema)?));
321 }
322
323 Ok(BlockData::Indexed32(result))
324}
325
326fn read_list16(r: &mut Reader, schema: &BlockSchema) -> Result<BlockData, UnpackError> {
327 let count = r.u16()?;
328 let mut result = Vec::with_capacity(count as usize);
329
330 for _ in 0..count {
331 result.push(read_row(r, schema)?);
332 }
333
334 Ok(BlockData::List16(result))
335}
336
337fn read_indexed_no_null8(r: &mut Reader, schema: &BlockSchema) -> Result<BlockData, UnpackError> {
338 let count = r.u8()?;
339 let mut result = IndexMap::new();
340
341 for _ in 0..count {
342 let index = r.u8()?;
343
344 result.insert(index, read_row(r, schema)?);
345 }
346
347 Ok(BlockData::IndexedNoNull8(result))
348}
349
350pub fn round2_f64(v: f32) -> f64 {
355 ((v as f64) * 100.0).round() / 100.0
356}
357
358pub fn to_base36(mut v: u32) -> String {
360 const DIGITS: &[u8; 36] = b"0123456789abcdefghijklmnopqrstuvwxyz";
361
362 if v == 0 {
363 return "0".to_string();
364 }
365
366 let mut out = Vec::new();
367
368 while v > 0 {
369 out.push(DIGITS[(v % 36) as usize]);
370 v /= 36;
371 }
372
373 out.reverse();
374 String::from_utf8(out).unwrap()
375}
376
377pub fn camera_to_json(camera: Option<&DecodedCamera>) -> Value {
379 let Some(camera) = camera else {
380 return json!(0);
381 };
382
383 let mut arr = vec![
384 Value::from(round2_f64(camera.x)),
385 Value::from(round2_f64(camera.y)),
386 ];
387
388 if camera.force_reset {
389 arr.push(Value::from(true));
390 }
391
392 if let Some(shake) = &camera.shake {
393 while arr.len() < 3 {
395 arr.push(Value::Null);
396 }
397
398 arr.push(Value::from(shake.clone()));
399 }
400
401 Value::Array(arr)
402}
403
404fn field_value_to_json(value: FieldValue) -> Value {
405 match value {
406 FieldValue::F32(v) => Value::from(round2_f64(v)),
407 FieldValue::U8(v) => Value::from(v),
408 FieldValue::U16(v) => Value::from(v),
409 FieldValue::U32(v) => Value::from(v),
410 }
411}
412
413fn row_to_json(fields: &[FieldValue]) -> Value {
416 Value::Array(fields.iter().copied().map(field_value_to_json).collect())
417}
418
419pub fn snapshot_to_json(snapshot: &DecodedSnapshot) -> Map<String, Value> {
425 let mut result = Map::new();
426
427 for block in &snapshot.blocks {
428 let value = match &block.data {
429 BlockData::Indexed8(items) => {
430 let mut map = Map::new();
431
432 for (id, row) in items {
433 map.insert(
434 id.to_string(),
435 row.as_deref().map_or(Value::Null, row_to_json),
436 );
437 }
438
439 Value::Object(map)
440 }
441 BlockData::Indexed32(items) => {
442 let mut map = Map::new();
443
444 for (id, row) in items {
445 map.insert(
446 to_base36(*id),
447 row.as_deref().map_or(Value::Null, row_to_json),
448 );
449 }
450
451 Value::Object(map)
452 }
453 BlockData::List16(items) => {
454 Value::Array(items.iter().map(|fields| row_to_json(fields)).collect())
455 }
456 BlockData::IndexedNoNull8(items) => {
457 let mut map = Map::new();
458
459 for (index, fields) in items {
460 map.insert(format!("d{index}"), row_to_json(fields));
461 }
462
463 Value::Object(map)
464 }
465 };
466
467 result.insert(block.key.clone(), value);
468 }
469
470 result
471}
472
473pub fn frame_to_json(frame: &DecodedFrame) -> Value {
476 let player = frame.player.as_ref().map_or(Value::Null, |p| {
477 json!({
478 "gameId": p.game_id,
479 "inputSeq": p.input_seq,
480 "state": p.state.iter().map(|v| *v as f64).collect::<Vec<f64>>(),
482 "centering": p.centering,
483 })
484 });
485
486 json!({
487 "port": frame.port,
488 "seq": frame.seq,
489 "serverTime": frame.server_time,
490 "camera": camera_to_json(frame.camera.as_ref()),
491 "player": player,
492 "snapshot": Value::Object(snapshot_to_json(&frame.snapshot)),
493 })
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499 use crate::config::test_support::full_snapshot_config;
500 use crate::snapshot::{Block, CameraData, PlayerBlock, SnapshotPacker};
501
502 fn test_config() -> SnapshotConfig {
505 full_snapshot_config(3, 5)
506 }
507
508 fn packed_frame(
509 blocks: &[(String, Block)],
510 camera: Option<&CameraData>,
511 player: Option<&PlayerBlock>,
512 ) -> Vec<u8> {
513 let mut packer = SnapshotPacker::new(test_config());
514
515 packer.pack_body(blocks).unwrap();
516 packer.pack_frame(1234.5, 42, camera, player).to_vec()
517 }
518
519 fn tank_row(floats: [f32; 7], condition: u8, size: u8, team: u8) -> Vec<FieldValue> {
520 let mut fields: Vec<FieldValue> = floats.iter().copied().map(FieldValue::F32).collect();
521
522 fields.push(FieldValue::U8(condition));
523 fields.push(FieldValue::U8(size));
524 fields.push(FieldValue::U8(team));
525 fields
526 }
527
528 fn tracer_row(floats: [f32; 6], was_hit: bool, shooter: u8) -> Vec<FieldValue> {
529 let mut fields: Vec<FieldValue> = floats.iter().copied().map(FieldValue::F32).collect();
530
531 fields.push(FieldValue::U8(was_hit as u8));
532 fields.push(FieldValue::U8(shooter));
533 fields
534 }
535
536 fn bomb_row(x: f32, y: f32, angle: f32, size: u8, time: u16, owner: u8) -> Vec<FieldValue> {
537 vec![
538 FieldValue::F32(x),
539 FieldValue::F32(y),
540 FieldValue::F32(angle),
541 FieldValue::U8(size),
542 FieldValue::U16(time),
543 FieldValue::U8(owner),
544 ]
545 }
546
547 fn explosion_row(x: f32, y: f32, radius: f32) -> Vec<FieldValue> {
548 vec![FieldValue::F32(x), FieldValue::F32(y), FieldValue::F32(radius)]
549 }
550
551 fn field_f32(fields: &[FieldValue], i: usize) -> f32 {
552 match fields[i] {
553 FieldValue::F32(v) => v,
554 _ => panic!("поле {i} не F32"),
555 }
556 }
557
558 #[test]
559 fn full_frame_round_trip() {
560 let blocks = vec![
561 (
562 "m1".to_string(),
563 Block::Indexed8(vec![
564 (
565 2,
566 Some(tank_row(
567 [10.567, -3.141, 1.5, 0.25, 100.0, -50.5, 1.2],
568 3,
569 2,
570 1,
571 )),
572 ),
573 (3, None),
574 ]),
575 ),
576 (
577 "w1".to_string(),
578 Block::List16(vec![tracer_row(
579 [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
580 true,
581 7,
582 )]),
583 ),
584 (
585 "w2".to_string(),
586 Block::Indexed32(vec![
587 (
588 u32::from_str_radix("a1f", 36).unwrap(),
590 Some(bomb_row(5.5, 6.5, 0.0, 8, 300, 2)),
591 ),
592 (1, None),
593 ]),
594 ),
595 (
596 "w2e".to_string(),
597 Block::List16(vec![explosion_row(100.0, 200.0, 50.0)]),
598 ),
599 (
600 "c1".to_string(),
601 Block::IndexedNoNull8(vec![(0, explosion_row(10.0, 20.0, 0.5))]),
602 ),
603 ];
604
605 let camera = CameraData {
606 x: 10.5,
607 y: -3.25,
608 force_reset: true,
609 shake: Some("20:200".to_string()),
610 };
611 let player = PlayerBlock {
612 game_id: 2,
613 input_seq: 77,
614 state: [10.56789, 2.0, 0.5, 3.0, 4.0, 0.1, 0.2, 0.7],
615 centering: true,
616 };
617
618 let data = packed_frame(&blocks, Some(&camera), Some(&player));
619 let frame = unpack_frame(&data, &test_config()).ok().unwrap();
620
621 assert_eq!(frame.port, 5);
622 assert_eq!(frame.seq, 42);
623 assert_eq!(frame.server_time, 1234.5);
624
625 let cam = frame.camera.as_ref().unwrap();
627
628 assert_eq!(cam.x, 10.5);
629 assert_eq!(cam.y, -3.25);
630 assert!(cam.force_reset);
631 assert_eq!(cam.shake.as_deref(), Some("20:200"));
632
633 let p = frame.player.as_ref().unwrap();
635
636 assert_eq!(p.game_id, 2);
637 assert_eq!(p.input_seq, 77);
638 assert_eq!(p.state[0], 10.56789f32);
639 assert!(p.centering);
640
641 let Some(BlockData::Indexed8(tanks)) = frame.snapshot.block_by_key("m1") else {
643 panic!("нет блока m1");
644 };
645 let row = tanks[&2].as_ref().unwrap();
646
647 assert_eq!(field_f32(row, 0), 10.57);
648 assert_eq!(field_f32(row, 1), -3.14);
649
650 match (row[7], row[8], row[9]) {
651 (FieldValue::U8(condition), FieldValue::U8(size), FieldValue::U8(team)) => {
652 assert_eq!((condition, size, team), (3, 2, 1));
653 }
654 _ => panic!("неожиданные типы полей tank"),
655 }
656 assert!(tanks[&3].is_none()); let Some(BlockData::List16(tracers)) = frame.snapshot.block_by_key("w1") else {
659 panic!("нет блока w1");
660 };
661
662 match tracers[0][6] {
663 FieldValue::U8(v) => assert_eq!(v, 1),
664 _ => panic!("поле 6 не U8"),
665 }
666 match tracers[0][7] {
667 FieldValue::U8(v) => assert_eq!(v, 7),
668 _ => panic!("поле 7 не U8"),
669 }
670
671 let Some(BlockData::Indexed32(bombs)) = frame.snapshot.block_by_key("w2") else {
672 panic!("нет блока w2");
673 };
674 let bomb = bombs[&u32::from_str_radix("a1f", 36).unwrap()]
675 .as_ref()
676 .unwrap();
677
678 match (bomb[3], bomb[4], bomb[5]) {
679 (FieldValue::U8(size), FieldValue::U16(time), FieldValue::U8(owner)) => {
680 assert_eq!((size, time, owner), (8, 300, 2));
681 }
682 _ => panic!("неожиданные типы полей bomb"),
683 }
684 assert!(bombs[&1].is_none());
685
686 let Some(BlockData::List16(explosions)) = frame.snapshot.block_by_key("w2e") else {
687 panic!("нет блока w2e");
688 };
689
690 assert_eq!(
691 (
692 field_f32(&explosions[0], 0),
693 field_f32(&explosions[0], 1),
694 field_f32(&explosions[0], 2)
695 ),
696 (100.0, 200.0, 50.0)
697 );
698
699 let Some(BlockData::IndexedNoNull8(dynamics)) = frame.snapshot.block_by_key("c1") else {
700 panic!("нет блока c1");
701 };
702
703 let dyn_row = &dynamics[&0];
704
705 assert_eq!(
706 (
707 field_f32(dyn_row, 0),
708 field_f32(dyn_row, 1),
709 field_f32(dyn_row, 2)
710 ),
711 (10.0, 20.0, 0.5)
712 );
713
714 assert_eq!(dyn_row.len(), 6);
717 assert_eq!(
718 (
719 field_f32(dyn_row, 3),
720 field_f32(dyn_row, 4),
721 field_f32(dyn_row, 5)
722 ),
723 (0.0, 0.0, 0.0)
724 );
725 }
726
727 #[test]
728 fn dynamics_optional_tail_round_trip() {
729 let resting = vec![
730 FieldValue::F32(10.0),
731 FieldValue::F32(20.0),
732 FieldValue::F32(0.5),
733 ];
734 let moving = vec![
735 FieldValue::F32(30.0),
736 FieldValue::F32(40.0),
737 FieldValue::F32(1.5),
738 FieldValue::F32(-2.25),
739 FieldValue::F32(3.75),
740 FieldValue::F32(0.9),
741 ];
742
743 let data = packed_frame(
744 &[(
745 "c1".to_string(),
746 Block::IndexedNoNull8(vec![(0, resting), (1, moving)]),
747 )],
748 None,
749 None,
750 );
751 let frame = unpack_frame(&data, &test_config()).ok().unwrap();
752
753 let Some(BlockData::IndexedNoNull8(dynamics)) = frame.snapshot.block_by_key("c1") else {
754 panic!("нет блока c1");
755 };
756
757 assert_eq!(
759 (
760 field_f32(&dynamics[&0], 3),
761 field_f32(&dynamics[&0], 4),
762 field_f32(&dynamics[&0], 5)
763 ),
764 (0.0, 0.0, 0.0)
765 );
766
767 assert_eq!(
769 (
770 field_f32(&dynamics[&1], 0),
771 field_f32(&dynamics[&1], 3),
772 field_f32(&dynamics[&1], 4),
773 field_f32(&dynamics[&1], 5)
774 ),
775 (30.0, -2.25, 3.75, 0.9)
776 );
777
778 assert_eq!(data.len(), 15 + 1 + 1 + (2 + 12) + (2 + 12 + 12));
780 }
781
782 #[test]
783 fn wrong_version_is_rejected() {
784 let mut data = packed_frame(&[], None, None);
785
786 data[1] = 99;
787 assert!(matches!(
788 unpack_frame(&data, &test_config()),
789 Err(UnpackError::WrongVersion)
790 ));
791 }
792
793 #[test]
794 fn truncated_frame_is_error() {
795 let data = packed_frame(
796 &[(
797 "m1".to_string(),
798 Block::Indexed8(vec![(1, Some(tank_row([0.0; 7], 3, 2, 1)))]),
799 )],
800 None,
801 None,
802 );
803
804 assert!(matches!(
805 unpack_frame(&data[..data.len() - 4], &test_config()),
806 Err(UnpackError::Truncated)
807 ));
808 }
809
810 #[test]
811 fn unknown_block_id_drops_rest() {
812 let mut data = packed_frame(
813 &[(
814 "w2e".to_string(),
815 Block::List16(vec![explosion_row(1.0, 2.0, 3.0)]),
816 )],
817 None,
818 None,
819 );
820
821 data[15] = 200;
823
824 let frame = unpack_frame(&data, &test_config()).ok().unwrap();
825
826 assert!(frame.snapshot.blocks.is_empty());
827 }
828
829 #[test]
830 fn base36_matches_js() {
831 assert_eq!(to_base36(0), "0");
832 assert_eq!(to_base36(35), "z");
833 assert_eq!(to_base36(u32::from_str_radix("a1f", 36).unwrap()), "a1f");
834 }
835
836 #[test]
837 fn camera_json_forms() {
838 assert_eq!(camera_to_json(None), json!(0));
839
840 let plain = DecodedCamera {
841 x: 1.5,
842 y: 2.5,
843 force_reset: false,
844 shake: None,
845 };
846
847 assert_eq!(camera_to_json(Some(&plain)), json!([1.5, 2.5]));
848
849 let reset = DecodedCamera {
850 force_reset: true,
851 ..plain.clone()
852 };
853
854 assert_eq!(camera_to_json(Some(&reset)), json!([1.5, 2.5, true]));
855
856 let shake = DecodedCamera {
858 shake: Some("20:200".to_string()),
859 ..plain
860 };
861
862 assert_eq!(
863 camera_to_json(Some(&shake)),
864 json!([1.5, 2.5, null, "20:200"])
865 );
866 }
867
868 #[test]
869 fn snapshot_json_matches_unpack_frame_forms() {
870 let blocks = vec![
871 (
872 "m1".to_string(),
873 Block::Indexed8(vec![
874 (
875 2,
876 Some(tank_row([51.28, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0], 3, 2, 1)),
877 ),
878 (3, None),
879 ]),
880 ),
881 (
882 "w2".to_string(),
883 Block::Indexed32(vec![(
884 u32::from_str_radix("a1f", 36).unwrap(),
885 Some(bomb_row(1.0, 2.0, 0.0, 8, 300, 2)),
886 )]),
887 ),
888 ];
889 let data = packed_frame(&blocks, None, None);
890 let frame = unpack_frame(&data, &test_config()).ok().unwrap();
891 let game = Value::Object(snapshot_to_json(&frame.snapshot));
892
893 assert_eq!(
895 game,
896 json!({
897 "m1": { "2": [51.28, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 3, 2, 1], "3": null },
898 "w2": { "a1f": [1.0, 2.0, 0.0, 8, 300, 2] },
899 })
900 );
901 }
902}