Skip to main content

vimp_engine_core/
config.rs

1use indexmap::IndexMap;
2use serde::Deserialize;
3
4/// Движковая половина конфигурации ядра (Wasm Host ABI, PLAN.md §3.4):
5/// общая для любой игры, не знает про модели/оружие/панель. Игровая
6/// половина — `G::Config` (`crate::sim::GameDef::Config`), парсится и
7/// живёт в game-crate; корневой JSON, который собирает JS, имеет форму
8/// `{engine: {...}, game: {...}}` — сборка обоих кусков в один объект
9/// init-конфига делает `GameCore::new` в game-crate.
10#[derive(Clone, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct EngineConfig {
13    /// Интервал фиксированного шага физики (секунды, напр. 1/120).
14    pub time_step: f32,
15    /// Масштаб карты по умолчанию (перекрывается scale самой карты).
16    #[serde(default = "default_map_scale")]
17    pub map_scale: f32,
18    /// Дефолтный setId конструктора карт (game.js mapSetId).
19    #[serde(default = "default_map_set_id")]
20    pub map_set_id: String,
21    pub snapshot: SnapshotConfig,
22    /// Сид PRNG ботов/разброса (детерминизм воспроизводим при равном сиде).
23    #[serde(default = "default_seed")]
24    pub seed: u64,
25}
26
27/// Движковая половина клиентского конфига (игровая половина — конфиг
28/// предиктора/шот-предиктора игры, напр. `TanksClientConfig`).
29#[derive(Clone, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub struct EngineClientConfig {
32    /// Шаг симуляции предикта (миллисекунды; EngineConfig.timeStep — секунды).
33    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 // произвольная константа
48}
49
50/// Форма блока в бинарном снапшоте (kind из src/config/opcodes.js):
51/// ширина id/count и наличие null-маркера для удалённых строк. Раскладка
52/// ПОЛЕЙ внутри строки — генерик, описывается отдельно схемой `fields`
53/// (см. `BlockSchema`) и не зашита в Rust; движок не знает игровых имён
54/// сущностей (танк/снаряд/трассер), только форму их строк.
55#[derive(Clone, Copy, PartialEq, Eq, Debug, Deserialize)]
56#[serde(rename_all = "camelCase")]
57pub enum BlockKind {
58    /// u8 id + null-маркер (удаление строки), u8 count.
59    Indexed8,
60    /// u32 id + null-маркер, u16 count.
61    Indexed32,
62    /// без id, без null-маркера, u16 count.
63    List16,
64    /// u8 индекс без null-маркера, u8 count.
65    IndexedNoNull8,
66}
67
68/// Бинарный тип поля строки блока.
69#[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/// Способ интерполяции поля на клиенте между кадрами A/B
79/// (применяется только к блокам класса `Hot` — см. `BlockClass`).
80#[derive(Clone, Copy, PartialEq, Eq, Deserialize)]
81#[serde(rename_all = "camelCase")]
82pub enum Interp {
83    Lerp,
84    LerpAngle,
85    Discrete,
86}
87
88/// Описание одного поля строки блока — порядок в векторе равен порядку
89/// байтов в раскладке (и порядку полей в конкретной Row-структуре
90/// core/src/snapshot.rs — молчаливый контракт, проверяемый тестами).
91#[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/// Значение поля строки во время упаковки/распаковки (рантайм-парность
105/// `FieldType`).
106#[derive(Clone, Copy)]
107pub enum FieldValue {
108    F32(f32),
109    U8(u8),
110    U16(u16),
111    U32(u32),
112}
113
114/// Класс блока: «горячий» — интерполируется клиентом между кадрами
115/// (танки, динамика карты), «событийный» — одноразовый, кадром как есть
116/// (трассеры/бомбы/взрывы).
117#[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
134/// Длина player-блока предикшена (predicted player state), см.
135/// `Tank::prediction_state`. Единая константа вместо 4 независимых
136/// литералов `8` (snapshot.rs/unpack.rs/tank.rs/predictor.rs).
137pub const PLAYER_STATE_LEN: usize = 8;
138
139/// Реестр снапшот-ключей и версия формата (src/config/opcodes.js).
140#[derive(Clone, Deserialize)]
141#[serde(rename_all = "camelCase")]
142pub struct SnapshotConfig {
143    pub version: u8,
144    /// Номер порта SHOT_DATA (первый байт кадра).
145    pub port: u8,
146    pub keys: IndexMap<String, BlockSchema>,
147}
148
149impl SnapshotConfig {
150    /// Проверяет структурные инварианты, общие для любой игры: `id` блока —
151    /// это опкод в бинарном кадре (см. `core/src/snapshot.rs`), совпадение
152    /// двух `id` молча портит распаковку на клиенте. Должна вызываться на
153    /// границе конструирования (`GameCore::new`/`ClientCore::new`).
154    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/// Настройки snapshot-интерполяции (src/config/client.js interpolation).
173#[derive(Clone, Deserialize)]
174#[serde(rename_all = "camelCase")]
175pub struct InterpolationConfig {
176    /// Задержка рендера в прошлом (мс).
177    pub delay: f64,
178    /// Максимальный возраст кадра в буфере (мс).
179    pub max_frame_age: f64,
180}
181
182/// Канонические схемы 5 сегодняшних kind — общий тестовый фикстур для
183/// snapshot.rs/unpack.rs/interpolator.rs/client/mod.rs (единый source of
184/// truth вместо копипаста SnapshotConfig в каждом test-модуле).
185#[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    /// Реестр из 5 ключей — 5 из 6 канонических ключей opcodes.js
286    /// (без `c2`, второго динамического слоя с идентичной `c1` схемой).
287    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}