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}
172
173pub const PLAYER_STATE_LEN: usize = 8;
177
178#[derive(Clone, Deserialize)]
180#[serde(rename_all = "camelCase")]
181pub struct SnapshotConfig {
182 pub version: u8,
183 pub port: u8,
185 pub keys: IndexMap<String, BlockSchema>,
186}
187
188impl SnapshotConfig {
189 pub fn validate(&self) -> Result<(), String> {
194 let mut seen_ids: Vec<u8> = Vec::new();
195
196 for (key, schema) in &self.keys {
197 if seen_ids.contains(&schema.id) {
198 return Err(format!(
199 "[core snapshot] Ключ '{key}': id {} уже используется другим блоком",
200 schema.id
201 ));
202 }
203
204 seen_ids.push(schema.id);
205 }
206
207 Ok(())
208 }
209}
210
211#[derive(Clone, Deserialize)]
213#[serde(rename_all = "camelCase")]
214pub struct InterpolationConfig {
215 pub delay: f64,
217 pub max_frame_age: f64,
219}
220
221#[cfg(test)]
225pub mod test_support {
226 use super::*;
227
228 fn field(name: &str, ty: FieldType) -> FieldSchema {
229 FieldSchema {
230 name: name.to_string(),
231 ty,
232 interp: Interp::Discrete,
233 }
234 }
235
236 fn field_interp(name: &str, ty: FieldType, interp: Interp) -> FieldSchema {
237 FieldSchema {
238 name: name.to_string(),
239 ty,
240 interp,
241 }
242 }
243
244 pub fn tanks_schema(id: u8) -> BlockSchema {
245 BlockSchema {
246 id,
247 kind: BlockKind::Indexed8,
248 class: BlockClass::Hot,
249 fields: vec![
250 field_interp("x", FieldType::F32, Interp::Lerp),
251 field_interp("y", FieldType::F32, Interp::Lerp),
252 field_interp("angle", FieldType::F32, Interp::LerpAngle),
253 field_interp("gunRotation", FieldType::F32, Interp::LerpAngle),
254 field_interp("vx", FieldType::F32, Interp::Lerp),
255 field_interp("vy", FieldType::F32, Interp::Lerp),
256 field_interp("engineLoad", FieldType::F32, Interp::Lerp),
257 field("condition", FieldType::U8),
258 field("size", FieldType::U8),
259 field("team", FieldType::U8),
260 ],
261 }
262 }
263
264 pub fn tracers_schema(id: u8) -> BlockSchema {
265 BlockSchema {
266 id,
267 kind: BlockKind::List16,
268 class: BlockClass::Event,
269 fields: vec![
270 field("startX", FieldType::F32),
271 field("startY", FieldType::F32),
272 field("endX", FieldType::F32),
273 field("endY", FieldType::F32),
274 field("bodyX", FieldType::F32),
275 field("bodyY", FieldType::F32),
276 field("wasHit", FieldType::U8),
277 field("shooterId", FieldType::U8),
278 ],
279 }
280 }
281
282 pub fn bombs_schema(id: u8) -> BlockSchema {
283 BlockSchema {
284 id,
285 kind: BlockKind::Indexed32,
286 class: BlockClass::Event,
287 fields: vec![
288 field("x", FieldType::F32),
289 field("y", FieldType::F32),
290 field("angle", FieldType::F32),
291 field("size", FieldType::U8),
292 field("time", FieldType::U16),
293 field("ownerId", FieldType::U8),
294 ],
295 }
296 }
297
298 pub fn explosions_schema(id: u8) -> BlockSchema {
299 BlockSchema {
300 id,
301 kind: BlockKind::List16,
302 class: BlockClass::Event,
303 fields: vec![
304 field("x", FieldType::F32),
305 field("y", FieldType::F32),
306 field("radius", FieldType::F32),
307 ],
308 }
309 }
310
311 pub fn dynamics_schema(id: u8) -> BlockSchema {
312 BlockSchema {
313 id,
314 kind: BlockKind::IndexedNoNull8,
315 class: BlockClass::Hot,
316 fields: vec![
317 field_interp("x", FieldType::F32, Interp::Lerp),
318 field_interp("y", FieldType::F32, Interp::Lerp),
319 field_interp("angle", FieldType::F32, Interp::LerpAngle),
320 ],
321 }
322 }
323
324 pub fn full_snapshot_config(version: u8, port: u8) -> SnapshotConfig {
327 let mut keys = IndexMap::new();
328
329 keys.insert("m1".to_string(), tanks_schema(1));
330 keys.insert("w1".to_string(), tracers_schema(2));
331 keys.insert("w2".to_string(), bombs_schema(3));
332 keys.insert("w2e".to_string(), explosions_schema(4));
333 keys.insert("c1".to_string(), dynamics_schema(5));
334
335 SnapshotConfig {
336 version,
337 port,
338 keys,
339 }
340 }
341}
342
343#[cfg(test)]
344mod validate_tests {
345 use super::test_support::full_snapshot_config;
346
347 #[test]
348 fn valid_schema_passes() {
349 assert!(full_snapshot_config(3, 5).validate().is_ok());
350 }
351
352 #[test]
353 fn duplicate_id_fails() {
354 let mut cfg = full_snapshot_config(3, 5);
355
356 cfg.keys.get_mut("w2").unwrap().id = cfg.keys["m1"].id;
357
358 let err = cfg.validate().unwrap_err();
359 assert!(err.contains("w2"));
360 }
361}