1use indexmap::IndexMap;
2use serde::Deserialize;
3
4#[derive(Clone, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct EngineConfig {
13 pub time_step: f32,
15 #[serde(default = "default_map_scale")]
17 pub map_scale: f32,
18 #[serde(default = "default_map_set_id")]
20 pub map_set_id: String,
21 pub snapshot: SnapshotConfig,
22 #[serde(default = "default_seed")]
24 pub seed: u64,
25}
26
27#[derive(Clone, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub struct EngineClientConfig {
32 pub time_step_ms: f64,
34 pub snapshot: SnapshotConfig,
35 pub interpolation: InterpolationConfig,
36 #[serde(default)]
40 pub divergence: Option<DivergenceConfig>,
41}
42
43#[derive(Clone, Deserialize)]
48#[serde(rename_all = "camelCase")]
49pub struct DivergenceConfig {
50 #[serde(default)]
51 pub thresholds: Vec<f32>,
52 #[serde(default = "default_divergence_threshold")]
53 pub default_threshold: f32,
54 #[serde(default = "default_divergence_capacity")]
57 pub capacity: usize,
58}
59
60impl DivergenceConfig {
61 pub fn threshold(&self, index: usize) -> f32 {
62 self.thresholds
63 .get(index)
64 .copied()
65 .unwrap_or(self.default_threshold)
66 }
67}
68
69fn default_divergence_threshold() -> f32 {
70 1.0
71}
72
73fn default_divergence_capacity() -> usize {
74 64
75}
76
77fn default_map_scale() -> f32 {
78 1.0
79}
80
81fn default_map_set_id() -> String {
82 "c1".to_string()
83}
84
85fn default_seed() -> u64 {
86 0x5644_4d49_5056_494d }
88
89#[derive(Clone, Copy, PartialEq, Eq, Debug, Deserialize)]
95#[serde(rename_all = "camelCase")]
96pub enum BlockKind {
97 Indexed8,
99 Indexed32,
101 List16,
103 IndexedNoNull8,
105}
106
107#[derive(Clone, Copy, PartialEq, Eq, Debug, Deserialize)]
109#[serde(rename_all = "lowercase")]
110pub enum FieldType {
111 F32,
112 U8,
113 U16,
114 U32,
115}
116
117#[derive(Clone, Copy, PartialEq, Eq, Deserialize)]
120#[serde(rename_all = "camelCase")]
121pub enum Interp {
122 Lerp,
123 LerpAngle,
124 Discrete,
125}
126
127#[derive(Clone, Deserialize)]
131#[serde(rename_all = "camelCase")]
132pub struct FieldSchema {
133 pub name: String,
134 pub ty: FieldType,
135 #[serde(default = "default_interp")]
136 pub interp: Interp,
137}
138
139fn default_interp() -> Interp {
140 Interp::Discrete
141}
142
143#[derive(Clone, Copy)]
146pub enum FieldValue {
147 F32(f32),
148 U8(u8),
149 U16(u16),
150 U32(u32),
151}
152
153#[derive(Clone, Copy, PartialEq, Eq, Deserialize)]
157#[serde(rename_all = "camelCase")]
158pub enum BlockClass {
159 Hot,
160 Event,
161}
162
163#[derive(Clone, Deserialize)]
164#[serde(rename_all = "camelCase")]
165pub struct BlockSchema {
166 pub id: u8,
167 pub kind: BlockKind,
168 pub class: BlockClass,
169 #[serde(default)]
170 pub fields: Vec<FieldSchema>,
171 #[serde(default)]
180 pub optional_from: Option<usize>,
181}
182
183impl BlockSchema {
184 pub fn required_len(&self) -> usize {
186 self.optional_from.unwrap_or(self.fields.len())
187 }
188}
189
190pub const PLAYER_STATE_LEN: usize = 8;
194
195#[derive(Clone, Deserialize)]
197#[serde(rename_all = "camelCase")]
198pub struct SnapshotConfig {
199 pub version: u8,
200 pub port: u8,
202 pub keys: IndexMap<String, BlockSchema>,
203}
204
205impl SnapshotConfig {
206 pub fn validate(&self) -> Result<(), String> {
211 let mut seen_ids: Vec<u8> = Vec::new();
212
213 for (key, schema) in &self.keys {
214 if seen_ids.contains(&schema.id) {
215 return Err(format!(
216 "[core snapshot] Ключ '{key}': id {} уже используется другим блоком",
217 schema.id
218 ));
219 }
220
221 seen_ids.push(schema.id);
222
223 if let Some(from) = schema.optional_from
227 && (from == 0 || from >= schema.fields.len())
228 {
229 return Err(format!(
230 "[core snapshot] Ключ '{key}': optionalFrom {from} вне диапазона \
231 1..{} — опциональный хвост должен быть непустым, а обязательная \
232 часть строки не может быть пустой",
233 schema.fields.len().saturating_sub(1)
234 ));
235 }
236 }
237
238 Ok(())
239 }
240}
241
242#[derive(Clone, Deserialize)]
244#[serde(rename_all = "camelCase")]
245pub struct InterpolationConfig {
246 pub delay: f64,
248 pub max_frame_age: f64,
250}
251
252#[cfg(test)]
256pub mod test_support {
257 use super::*;
258
259 fn field(name: &str, ty: FieldType) -> FieldSchema {
260 FieldSchema {
261 name: name.to_string(),
262 ty,
263 interp: Interp::Discrete,
264 }
265 }
266
267 fn field_interp(name: &str, ty: FieldType, interp: Interp) -> FieldSchema {
268 FieldSchema {
269 name: name.to_string(),
270 ty,
271 interp,
272 }
273 }
274
275 pub fn tanks_schema(id: u8) -> BlockSchema {
276 BlockSchema {
277 id,
278 kind: BlockKind::Indexed8,
279 class: BlockClass::Hot,
280 optional_from: None,
281 fields: vec![
282 field_interp("x", FieldType::F32, Interp::Lerp),
283 field_interp("y", FieldType::F32, Interp::Lerp),
284 field_interp("angle", FieldType::F32, Interp::LerpAngle),
285 field_interp("gunRotation", FieldType::F32, Interp::LerpAngle),
286 field_interp("vx", FieldType::F32, Interp::Lerp),
287 field_interp("vy", FieldType::F32, Interp::Lerp),
288 field_interp("engineLoad", FieldType::F32, Interp::Lerp),
289 field("condition", FieldType::U8),
290 field("size", FieldType::U8),
291 field("team", FieldType::U8),
292 ],
293 }
294 }
295
296 pub fn tracers_schema(id: u8) -> BlockSchema {
297 BlockSchema {
298 id,
299 kind: BlockKind::List16,
300 class: BlockClass::Event,
301 optional_from: None,
302 fields: vec![
303 field("startX", FieldType::F32),
304 field("startY", FieldType::F32),
305 field("endX", FieldType::F32),
306 field("endY", FieldType::F32),
307 field("bodyX", FieldType::F32),
308 field("bodyY", FieldType::F32),
309 field("wasHit", FieldType::U8),
310 field("shooterId", FieldType::U8),
311 ],
312 }
313 }
314
315 pub fn bombs_schema(id: u8) -> BlockSchema {
316 BlockSchema {
317 id,
318 kind: BlockKind::Indexed32,
319 class: BlockClass::Event,
320 optional_from: None,
321 fields: vec![
322 field("x", FieldType::F32),
323 field("y", FieldType::F32),
324 field("angle", FieldType::F32),
325 field("size", FieldType::U8),
326 field("time", FieldType::U16),
327 field("ownerId", FieldType::U8),
328 ],
329 }
330 }
331
332 pub fn explosions_schema(id: u8) -> BlockSchema {
333 BlockSchema {
334 id,
335 kind: BlockKind::List16,
336 class: BlockClass::Event,
337 optional_from: None,
338 fields: vec![
339 field("x", FieldType::F32),
340 field("y", FieldType::F32),
341 field("radius", FieldType::F32),
342 ],
343 }
344 }
345
346 pub fn dynamics_schema(id: u8) -> BlockSchema {
349 BlockSchema {
350 id,
351 kind: BlockKind::IndexedNoNull8,
352 class: BlockClass::Hot,
353 optional_from: Some(3),
354 fields: vec![
355 field_interp("x", FieldType::F32, Interp::Lerp),
356 field_interp("y", FieldType::F32, Interp::Lerp),
357 field_interp("angle", FieldType::F32, Interp::LerpAngle),
358 field_interp("vx", FieldType::F32, Interp::Lerp),
359 field_interp("vy", FieldType::F32, Interp::Lerp),
360 field_interp("angvel", FieldType::F32, Interp::Lerp),
362 ],
363 }
364 }
365
366 pub fn full_snapshot_config(version: u8, port: u8) -> SnapshotConfig {
369 let mut keys = IndexMap::new();
370
371 keys.insert("m1".to_string(), tanks_schema(1));
372 keys.insert("w1".to_string(), tracers_schema(2));
373 keys.insert("w2".to_string(), bombs_schema(3));
374 keys.insert("w2e".to_string(), explosions_schema(4));
375 keys.insert("c1".to_string(), dynamics_schema(5));
376
377 SnapshotConfig {
378 version,
379 port,
380 keys,
381 }
382 }
383}
384
385#[cfg(test)]
386mod validate_tests {
387 use super::test_support::full_snapshot_config;
388
389 #[test]
390 fn valid_schema_passes() {
391 assert!(full_snapshot_config(3, 5).validate().is_ok());
392 }
393
394 #[test]
395 fn optional_from_out_of_range_fails() {
396 let mut cfg = full_snapshot_config(3, 5);
397
398 cfg.keys.get_mut("c1").unwrap().optional_from = Some(6);
399
400 let err = cfg.validate().unwrap_err();
401 assert!(err.contains("optionalFrom"));
402 }
403
404 #[test]
405 fn optional_from_zero_fails() {
406 let mut cfg = full_snapshot_config(3, 5);
407
408 cfg.keys.get_mut("c1").unwrap().optional_from = Some(0);
409
410 assert!(cfg.validate().is_err());
411 }
412
413 #[test]
414 fn duplicate_id_fails() {
415 let mut cfg = full_snapshot_config(3, 5);
416
417 cfg.keys.get_mut("w2").unwrap().id = cfg.keys["m1"].id;
418
419 let err = cfg.validate().unwrap_err();
420 assert!(err.contains("w2"));
421 }
422}