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}
37
38fn default_map_scale() -> f32 {
39 1.0
40}
41
42fn default_map_set_id() -> String {
43 "c1".to_string()
44}
45
46fn default_seed() -> u64 {
47 0x5644_4d49_5056_494d }
49
50#[derive(Clone, Copy, PartialEq, Eq, Debug, Deserialize)]
56#[serde(rename_all = "camelCase")]
57pub enum BlockKind {
58 Indexed8,
60 Indexed32,
62 List16,
64 IndexedNoNull8,
66}
67
68#[derive(Clone, Copy, PartialEq, Eq, Debug, Deserialize)]
70#[serde(rename_all = "lowercase")]
71pub enum FieldType {
72 F32,
73 U8,
74 U16,
75 U32,
76}
77
78#[derive(Clone, Copy, PartialEq, Eq, Deserialize)]
81#[serde(rename_all = "camelCase")]
82pub enum Interp {
83 Lerp,
84 LerpAngle,
85 Discrete,
86}
87
88#[derive(Clone, Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct FieldSchema {
94 pub name: String,
95 pub ty: FieldType,
96 #[serde(default = "default_interp")]
97 pub interp: Interp,
98}
99
100fn default_interp() -> Interp {
101 Interp::Discrete
102}
103
104#[derive(Clone, Copy)]
107pub enum FieldValue {
108 F32(f32),
109 U8(u8),
110 U16(u16),
111 U32(u32),
112}
113
114#[derive(Clone, Copy, PartialEq, Eq, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub enum BlockClass {
120 Hot,
121 Event,
122}
123
124#[derive(Clone, Deserialize)]
125#[serde(rename_all = "camelCase")]
126pub struct BlockSchema {
127 pub id: u8,
128 pub kind: BlockKind,
129 pub class: BlockClass,
130 #[serde(default)]
131 pub fields: Vec<FieldSchema>,
132}
133
134pub const PLAYER_STATE_LEN: usize = 8;
138
139#[derive(Clone, Deserialize)]
141#[serde(rename_all = "camelCase")]
142pub struct SnapshotConfig {
143 pub version: u8,
144 pub port: u8,
146 pub keys: IndexMap<String, BlockSchema>,
147}
148
149impl SnapshotConfig {
150 pub fn validate(&self) -> Result<(), String> {
155 let mut seen_ids: Vec<u8> = Vec::new();
156
157 for (key, schema) in &self.keys {
158 if seen_ids.contains(&schema.id) {
159 return Err(format!(
160 "[core snapshot] Ключ '{key}': id {} уже используется другим блоком",
161 schema.id
162 ));
163 }
164
165 seen_ids.push(schema.id);
166 }
167
168 Ok(())
169 }
170}
171
172#[derive(Clone, Deserialize)]
174#[serde(rename_all = "camelCase")]
175pub struct InterpolationConfig {
176 pub delay: f64,
178 pub max_frame_age: f64,
180}
181
182#[cfg(test)]
186pub mod test_support {
187 use super::*;
188
189 fn field(name: &str, ty: FieldType) -> FieldSchema {
190 FieldSchema {
191 name: name.to_string(),
192 ty,
193 interp: Interp::Discrete,
194 }
195 }
196
197 fn field_interp(name: &str, ty: FieldType, interp: Interp) -> FieldSchema {
198 FieldSchema {
199 name: name.to_string(),
200 ty,
201 interp,
202 }
203 }
204
205 pub fn tanks_schema(id: u8) -> BlockSchema {
206 BlockSchema {
207 id,
208 kind: BlockKind::Indexed8,
209 class: BlockClass::Hot,
210 fields: vec![
211 field_interp("x", FieldType::F32, Interp::Lerp),
212 field_interp("y", FieldType::F32, Interp::Lerp),
213 field_interp("angle", FieldType::F32, Interp::LerpAngle),
214 field_interp("gunRotation", FieldType::F32, Interp::LerpAngle),
215 field_interp("vx", FieldType::F32, Interp::Lerp),
216 field_interp("vy", FieldType::F32, Interp::Lerp),
217 field_interp("engineLoad", FieldType::F32, Interp::Lerp),
218 field("condition", FieldType::U8),
219 field("size", FieldType::U8),
220 field("team", FieldType::U8),
221 ],
222 }
223 }
224
225 pub fn tracers_schema(id: u8) -> BlockSchema {
226 BlockSchema {
227 id,
228 kind: BlockKind::List16,
229 class: BlockClass::Event,
230 fields: vec![
231 field("startX", FieldType::F32),
232 field("startY", FieldType::F32),
233 field("endX", FieldType::F32),
234 field("endY", FieldType::F32),
235 field("bodyX", FieldType::F32),
236 field("bodyY", FieldType::F32),
237 field("wasHit", FieldType::U8),
238 field("shooterId", FieldType::U8),
239 ],
240 }
241 }
242
243 pub fn bombs_schema(id: u8) -> BlockSchema {
244 BlockSchema {
245 id,
246 kind: BlockKind::Indexed32,
247 class: BlockClass::Event,
248 fields: vec![
249 field("x", FieldType::F32),
250 field("y", FieldType::F32),
251 field("angle", FieldType::F32),
252 field("size", FieldType::U8),
253 field("time", FieldType::U16),
254 field("ownerId", FieldType::U8),
255 ],
256 }
257 }
258
259 pub fn explosions_schema(id: u8) -> BlockSchema {
260 BlockSchema {
261 id,
262 kind: BlockKind::List16,
263 class: BlockClass::Event,
264 fields: vec![
265 field("x", FieldType::F32),
266 field("y", FieldType::F32),
267 field("radius", FieldType::F32),
268 ],
269 }
270 }
271
272 pub fn dynamics_schema(id: u8) -> BlockSchema {
273 BlockSchema {
274 id,
275 kind: BlockKind::IndexedNoNull8,
276 class: BlockClass::Hot,
277 fields: vec![
278 field_interp("x", FieldType::F32, Interp::Lerp),
279 field_interp("y", FieldType::F32, Interp::Lerp),
280 field_interp("angle", FieldType::F32, Interp::LerpAngle),
281 ],
282 }
283 }
284
285 pub fn full_snapshot_config(version: u8, port: u8) -> SnapshotConfig {
288 let mut keys = IndexMap::new();
289
290 keys.insert("m1".to_string(), tanks_schema(1));
291 keys.insert("w1".to_string(), tracers_schema(2));
292 keys.insert("w2".to_string(), bombs_schema(3));
293 keys.insert("w2e".to_string(), explosions_schema(4));
294 keys.insert("c1".to_string(), dynamics_schema(5));
295
296 SnapshotConfig {
297 version,
298 port,
299 keys,
300 }
301 }
302}
303
304#[cfg(test)]
305mod validate_tests {
306 use super::test_support::full_snapshot_config;
307
308 #[test]
309 fn valid_schema_passes() {
310 assert!(full_snapshot_config(3, 5).validate().is_ok());
311 }
312
313 #[test]
314 fn duplicate_id_fails() {
315 let mut cfg = full_snapshot_config(3, 5);
316
317 cfg.keys.get_mut("w2").unwrap().id = cfg.keys["m1"].id;
318
319 let err = cfg.validate().unwrap_err();
320 assert!(err.contains("w2"));
321 }
322}