1use crate::config::{
2 BlockKind, BlockSchema, FieldType, FieldValue, PLAYER_STATE_LEN, SnapshotConfig,
3};
4
5const CAMERA_FLAG_HAS_CAMERA: u8 = 1;
10const CAMERA_FLAG_FORCE_RESET: u8 = 2;
11const CAMERA_FLAG_HAS_SHAKE: u8 = 4;
12const CAMERA_FLAG_HAS_PLAYER: u8 = 8;
13
14pub enum Block {
19 Indexed8(Vec<(u8, Option<Vec<FieldValue>>)>),
21 Indexed32(Vec<(u32, Option<Vec<FieldValue>>)>),
23 List16(Vec<Vec<FieldValue>>),
25 IndexedNoNull8(Vec<(u8, Vec<FieldValue>)>),
27}
28
29impl Block {
30 fn kind(&self) -> BlockKind {
31 match self {
32 Block::Indexed8(_) => BlockKind::Indexed8,
33 Block::Indexed32(_) => BlockKind::Indexed32,
34 Block::List16(_) => BlockKind::List16,
35 Block::IndexedNoNull8(_) => BlockKind::IndexedNoNull8,
36 }
37 }
38}
39
40pub struct CameraData {
42 pub x: f32,
43 pub y: f32,
44 pub force_reset: bool,
45 pub shake: Option<String>,
46}
47
48pub struct PlayerBlock {
50 pub game_id: u8,
51 pub input_seq: u32,
52 pub state: [f32; PLAYER_STATE_LEN],
53 pub centering: bool,
54}
55
56pub struct SnapshotPacker {
57 cfg: SnapshotConfig,
58 body: Vec<u8>,
59 frame: Vec<u8>,
60}
61
62fn push_u16(buf: &mut Vec<u8>, v: u16) {
63 buf.extend_from_slice(&v.to_be_bytes());
64}
65
66fn push_u32(buf: &mut Vec<u8>, v: u32) {
67 buf.extend_from_slice(&v.to_be_bytes());
68}
69
70fn push_f32(buf: &mut Vec<u8>, v: f32) {
71 buf.extend_from_slice(&v.to_be_bytes());
72}
73
74fn push_f64(buf: &mut Vec<u8>, v: f64) {
75 buf.extend_from_slice(&v.to_be_bytes());
76}
77
78fn write_field(buf: &mut Vec<u8>, ty: FieldType, value: FieldValue) {
84 match (ty, value) {
85 (FieldType::F32, FieldValue::F32(v)) => push_f32(buf, v),
86 (FieldType::U8, FieldValue::U8(v)) => buf.push(v),
87 (FieldType::U16, FieldValue::U16(v)) => push_u16(buf, v),
88 (FieldType::U32, FieldValue::U32(v)) => push_u32(buf, v),
89 _ => unreachable!(
90 "[core snapshot] тип поля не совпадает со схемой — validate() должен был это отловить"
91 ),
92 }
93}
94
95impl SnapshotPacker {
96 pub fn new(cfg: SnapshotConfig) -> Self {
97 Self {
98 cfg,
99 body: Vec::with_capacity(4096),
100 frame: Vec::with_capacity(4096),
101 }
102 }
103
104 pub fn pack_body(&mut self, blocks: &[(String, Block)]) -> Result<(), String> {
107 self.body.clear();
108
109 for (key, block) in blocks {
110 let schema = self.cfg.keys.get(key).ok_or_else(|| {
111 format!(
112 "[core snapshot] Неизвестный ключ снапшота '{key}': \
113 зарегистрируйте его в src/config/opcodes.js"
114 )
115 })?;
116
117 if schema.kind != block.kind() {
118 return Err(format!(
119 "[core snapshot] Раскладка блока '{key}' не совпадает с kind из opcodes.js"
120 ));
121 }
122
123 self.body.push(schema.id);
124 Self::write_block(&mut self.body, schema, block);
125 }
126
127 Ok(())
128 }
129
130 fn write_block(buf: &mut Vec<u8>, schema: &BlockSchema, block: &Block) {
134 fn write_row(buf: &mut Vec<u8>, schema: &BlockSchema, fields: &[FieldValue]) {
140 debug_assert!(
141 fields.len() == schema.fields.len() || fields.len() == schema.required_len(),
142 "[core snapshot] длина строки {} не равна ни полной ширине схемы {}, \
143 ни её обязательной части {}",
144 fields.len(),
145 schema.fields.len(),
146 schema.required_len()
147 );
148
149 let len = if schema.optional_from.is_some() {
150 let has_tail = fields.len() > schema.required_len();
151
152 buf.push(has_tail as u8);
153
154 if has_tail {
155 schema.fields.len()
156 } else {
157 schema.required_len()
158 }
159 } else {
160 schema.fields.len()
161 };
162
163 for (i, field) in schema.fields.iter().take(len).enumerate() {
164 write_field(buf, field.ty, fields[i]);
165 }
166 }
167
168 match block {
169 Block::Indexed8(items) => {
170 buf.push(items.len() as u8);
171
172 for (id, row) in items {
173 buf.push(*id);
174
175 match row {
176 None => buf.push(0),
177 Some(fields) => {
178 buf.push(1);
179 write_row(buf, schema, fields);
180 }
181 }
182 }
183 }
184 Block::Indexed32(items) => {
185 push_u16(buf, items.len() as u16);
186
187 for (id, row) in items {
188 push_u32(buf, *id);
189
190 match row {
191 None => buf.push(0),
192 Some(fields) => {
193 buf.push(1);
194 write_row(buf, schema, fields);
195 }
196 }
197 }
198 }
199 Block::List16(items) => {
200 push_u16(buf, items.len() as u16);
201
202 for fields in items {
203 write_row(buf, schema, fields);
204 }
205 }
206 Block::IndexedNoNull8(items) => {
207 buf.push(items.len() as u8);
208
209 for (index, fields) in items {
210 buf.push(*index);
211 write_row(buf, schema, fields);
212 }
213 }
214 }
215 }
216
217 pub fn pack_frame(
220 &mut self,
221 server_time: f64,
222 seq: u32,
223 camera: Option<&CameraData>,
224 player: Option<&PlayerBlock>,
225 ) -> &[u8] {
226 let frame = &mut self.frame;
227
228 frame.clear();
229 frame.push(self.cfg.port);
230 frame.push(self.cfg.version);
231 push_u32(frame, seq);
232 push_f64(frame, server_time);
233
234 let mut flags = 0u8;
235 let shake = camera.and_then(|camera| camera.shake.as_deref());
236
237 if let Some(camera) = camera {
238 flags |= CAMERA_FLAG_HAS_CAMERA;
239
240 if camera.force_reset {
241 flags |= CAMERA_FLAG_FORCE_RESET;
242 }
243
244 if shake.is_some() {
245 flags |= CAMERA_FLAG_HAS_SHAKE;
246 }
247 }
248
249 if player.is_some() {
250 flags |= CAMERA_FLAG_HAS_PLAYER;
251 }
252
253 frame.push(flags);
254
255 if let Some(camera) = camera {
256 push_f32(frame, camera.x);
257 push_f32(frame, camera.y);
258 }
259
260 if let Some(shake) = shake {
261 frame.push(shake.len() as u8);
262 frame.extend_from_slice(shake.as_bytes());
263 }
264
265 if let Some(player) = player {
266 frame.push(player.game_id);
267 push_u32(frame, player.input_seq);
268
269 for value in player.state {
270 push_f32(frame, value);
271 }
272
273 frame.push(player.centering as u8);
274 }
275
276 frame.extend_from_slice(&self.body);
277
278 frame
279 }
280
281 pub fn frame_bytes(&self) -> &[u8] {
282 &self.frame
283 }
284
285 pub fn body_len(&self) -> usize {
286 self.body.len()
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293 use crate::config::test_support::{dynamics_schema, tanks_schema, tracers_schema};
294 use indexmap::IndexMap;
295
296 fn test_config() -> SnapshotConfig {
297 let mut keys = IndexMap::new();
298
299 keys.insert("m1".to_string(), tanks_schema(1));
300 keys.insert("w1".to_string(), tracers_schema(2));
301 keys.insert("c1".to_string(), dynamics_schema(5));
302
303 SnapshotConfig {
304 version: 3,
305 port: 5,
306 keys,
307 }
308 }
309
310 #[test]
311 fn frame_header_layout() {
312 let mut packer = SnapshotPacker::new(test_config());
313
314 packer.pack_body(&[]).unwrap();
315
316 let frame = packer.pack_frame(
317 1234.5,
318 42,
319 Some(&CameraData {
320 x: 10.5,
321 y: -3.25,
322 force_reset: true,
323 shake: Some("20:200".to_string()),
324 }),
325 None,
326 );
327
328 assert_eq!(frame[0], 5); assert_eq!(frame[1], 3); assert_eq!(u32::from_be_bytes(frame[2..6].try_into().unwrap()), 42);
331 assert_eq!(
332 f64::from_be_bytes(frame[6..14].try_into().unwrap()),
333 1234.5
334 );
335 assert_eq!(frame[14], 1 | 2 | 4);
337 assert_eq!(f32::from_be_bytes(frame[15..19].try_into().unwrap()), 10.5);
338 assert_eq!(frame[23], 6); assert_eq!(&frame[24..30], b"20:200");
340 }
341
342 #[test]
343 fn unknown_key_is_error() {
344 let mut packer = SnapshotPacker::new(test_config());
345 let result = packer.pack_body(&[("zzz".to_string(), Block::List16(vec![]))]);
346
347 assert!(result.is_err());
348 }
349
350 #[test]
351 fn tank_block_layout() {
352 let mut packer = SnapshotPacker::new(test_config());
353
354 packer
355 .pack_body(&[(
356 "m1".to_string(),
357 Block::Indexed8(vec![
358 (
359 7,
360 Some(vec![
361 FieldValue::F32(1.0),
362 FieldValue::F32(2.0),
363 FieldValue::F32(3.0),
364 FieldValue::F32(4.0),
365 FieldValue::F32(5.0),
366 FieldValue::F32(6.0),
367 FieldValue::F32(7.0),
368 FieldValue::U8(3),
369 FieldValue::U8(2),
370 FieldValue::U8(1),
371 ]),
372 ),
373 (9, None),
374 ]),
375 )])
376 .unwrap();
377
378 let frame = packer.pack_frame(0.0, 0, None, None).to_vec();
379 let body = &frame[15..]; assert_eq!(body[0], 1); assert_eq!(body[1], 2); assert_eq!(body[2], 7); assert_eq!(body[3], 1); assert_eq!(body[4 + 28], 3);
387 assert_eq!(body[4 + 29], 2);
388 assert_eq!(body[4 + 30], 1);
389 assert_eq!(body[4 + 31], 9); assert_eq!(body[4 + 32], 0); }
392
393 #[test]
396 fn dynamics_optional_tail_layout() {
397 let mut packer = SnapshotPacker::new(test_config());
398 let f32s = |values: &[f32]| -> Vec<FieldValue> {
399 values.iter().copied().map(FieldValue::F32).collect()
400 };
401
402 packer
403 .pack_body(&[(
404 "c1".to_string(),
405 Block::IndexedNoNull8(vec![
406 (0, f32s(&[1.0, 2.0, 3.0])),
407 (1, f32s(&[4.0, 5.0, 6.0, 7.0, 8.0, 9.0])),
408 ]),
409 )])
410 .unwrap();
411
412 let frame = packer.pack_frame(0.0, 0, None, None).to_vec();
413 let body = &frame[15..]; assert_eq!(body[0], 5); assert_eq!(body[1], 2); assert_eq!(body[2], 0); assert_eq!(body[3], 0); assert_eq!(f32::from_be_bytes(body[4..8].try_into().unwrap()), 1.0);
420
421 assert_eq!(body[16], 1); assert_eq!(body[17], 1); assert_eq!(f32::from_be_bytes(body[30..34].try_into().unwrap()), 7.0);
425 assert_eq!(body.len(), 2 + (2 + 12) + (2 + 24));
426 }
427}