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]) {
135 for (i, field) in schema.fields.iter().enumerate() {
136 write_field(buf, field.ty, fields[i]);
137 }
138 }
139
140 match block {
141 Block::Indexed8(items) => {
142 buf.push(items.len() as u8);
143
144 for (id, row) in items {
145 buf.push(*id);
146
147 match row {
148 None => buf.push(0),
149 Some(fields) => {
150 buf.push(1);
151 write_row(buf, schema, fields);
152 }
153 }
154 }
155 }
156 Block::Indexed32(items) => {
157 push_u16(buf, items.len() as u16);
158
159 for (id, row) in items {
160 push_u32(buf, *id);
161
162 match row {
163 None => buf.push(0),
164 Some(fields) => {
165 buf.push(1);
166 write_row(buf, schema, fields);
167 }
168 }
169 }
170 }
171 Block::List16(items) => {
172 push_u16(buf, items.len() as u16);
173
174 for fields in items {
175 write_row(buf, schema, fields);
176 }
177 }
178 Block::IndexedNoNull8(items) => {
179 buf.push(items.len() as u8);
180
181 for (index, fields) in items {
182 buf.push(*index);
183 write_row(buf, schema, fields);
184 }
185 }
186 }
187 }
188
189 pub fn pack_frame(
192 &mut self,
193 server_time: f64,
194 seq: u32,
195 camera: Option<&CameraData>,
196 player: Option<&PlayerBlock>,
197 ) -> &[u8] {
198 let frame = &mut self.frame;
199
200 frame.clear();
201 frame.push(self.cfg.port);
202 frame.push(self.cfg.version);
203 push_u32(frame, seq);
204 push_f64(frame, server_time);
205
206 let mut flags = 0u8;
207 let shake = camera.and_then(|camera| camera.shake.as_deref());
208
209 if let Some(camera) = camera {
210 flags |= CAMERA_FLAG_HAS_CAMERA;
211
212 if camera.force_reset {
213 flags |= CAMERA_FLAG_FORCE_RESET;
214 }
215
216 if shake.is_some() {
217 flags |= CAMERA_FLAG_HAS_SHAKE;
218 }
219 }
220
221 if player.is_some() {
222 flags |= CAMERA_FLAG_HAS_PLAYER;
223 }
224
225 frame.push(flags);
226
227 if let Some(camera) = camera {
228 push_f32(frame, camera.x);
229 push_f32(frame, camera.y);
230 }
231
232 if let Some(shake) = shake {
233 frame.push(shake.len() as u8);
234 frame.extend_from_slice(shake.as_bytes());
235 }
236
237 if let Some(player) = player {
238 frame.push(player.game_id);
239 push_u32(frame, player.input_seq);
240
241 for value in player.state {
242 push_f32(frame, value);
243 }
244
245 frame.push(player.centering as u8);
246 }
247
248 frame.extend_from_slice(&self.body);
249
250 frame
251 }
252
253 pub fn frame_bytes(&self) -> &[u8] {
254 &self.frame
255 }
256
257 pub fn body_len(&self) -> usize {
258 self.body.len()
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265 use crate::config::test_support::{tanks_schema, tracers_schema};
266 use indexmap::IndexMap;
267
268 fn test_config() -> SnapshotConfig {
269 let mut keys = IndexMap::new();
270
271 keys.insert("m1".to_string(), tanks_schema(1));
272 keys.insert("w1".to_string(), tracers_schema(2));
273
274 SnapshotConfig {
275 version: 3,
276 port: 5,
277 keys,
278 }
279 }
280
281 #[test]
282 fn frame_header_layout() {
283 let mut packer = SnapshotPacker::new(test_config());
284
285 packer.pack_body(&[]).unwrap();
286
287 let frame = packer.pack_frame(
288 1234.5,
289 42,
290 Some(&CameraData {
291 x: 10.5,
292 y: -3.25,
293 force_reset: true,
294 shake: Some("20:200".to_string()),
295 }),
296 None,
297 );
298
299 assert_eq!(frame[0], 5); assert_eq!(frame[1], 3); assert_eq!(u32::from_be_bytes(frame[2..6].try_into().unwrap()), 42);
302 assert_eq!(
303 f64::from_be_bytes(frame[6..14].try_into().unwrap()),
304 1234.5
305 );
306 assert_eq!(frame[14], 1 | 2 | 4);
308 assert_eq!(f32::from_be_bytes(frame[15..19].try_into().unwrap()), 10.5);
309 assert_eq!(frame[23], 6); assert_eq!(&frame[24..30], b"20:200");
311 }
312
313 #[test]
314 fn unknown_key_is_error() {
315 let mut packer = SnapshotPacker::new(test_config());
316 let result = packer.pack_body(&[("zzz".to_string(), Block::List16(vec![]))]);
317
318 assert!(result.is_err());
319 }
320
321 #[test]
322 fn tank_block_layout() {
323 let mut packer = SnapshotPacker::new(test_config());
324
325 packer
326 .pack_body(&[(
327 "m1".to_string(),
328 Block::Indexed8(vec![
329 (
330 7,
331 Some(vec![
332 FieldValue::F32(1.0),
333 FieldValue::F32(2.0),
334 FieldValue::F32(3.0),
335 FieldValue::F32(4.0),
336 FieldValue::F32(5.0),
337 FieldValue::F32(6.0),
338 FieldValue::F32(7.0),
339 FieldValue::U8(3),
340 FieldValue::U8(2),
341 FieldValue::U8(1),
342 ]),
343 ),
344 (9, None),
345 ]),
346 )])
347 .unwrap();
348
349 let frame = packer.pack_frame(0.0, 0, None, None).to_vec();
350 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);
358 assert_eq!(body[4 + 29], 2);
359 assert_eq!(body[4 + 30], 1);
360 assert_eq!(body[4 + 31], 9); assert_eq!(body[4 + 32], 0); }
363}