vimp_engine_core/abi.rs
1//! `export_game_core_abi!` — генерирует `#[wasm_bindgen] impl` для игрового
2//! `GameCore`, снимая ~25-метод boilerplate, зафиксированный §3.4 PLAN.md.
3//! Все методы ниже — механические 1:1-делегации в уже generic
4//! `EngineSim<G>`/`SnapshotPacker`, чья сигнатура зафиксирована трейтом
5//! `crate::sim::GameSim<G>` независимо от конкретной игры — не дизайн под
6//! гипотетическую вторую игру, а извлечение уже существующего соответствия
7//! (см. PLAN_4_details.md §5).
8//!
9//! Контракт: тип-цель обязан иметь поля `state: vimp_engine_core::game::EngineSim<G>`
10//! (под именем `EngineSim<...>`, конкретный `G` — забота игрового crate) и
11//! `packer: vimp_engine_core::snapshot::SnapshotPacker`, и раскрываться в
12//! модуле, где уже подключён `wasm-bindgen` как зависимость (пути ниже
13//! квалифицированы через `::wasm_bindgen`, поэтому не зависят от локальных
14//! `use`, но зависят от наличия крейта в `Cargo.toml` вызывающей стороны).
15//! `new` (парсинг игрового конфига) и не-`#[wasm_bindgen]` тестовые
16//! аксессоры в макрос не входят — остаются рукописными в game-crate.
17#[macro_export]
18macro_rules! export_game_core_abi {
19 ($GameCoreTy:ty) => {
20 #[::wasm_bindgen::prelude::wasm_bindgen]
21 impl $GameCoreTy {
22 /// Загружает карту из JSON (см. scripts/export-maps.js).
23 pub fn load_map(&mut self, map_json: &str) -> Result<(), ::wasm_bindgen::JsError> {
24 self.state
25 .load_map(map_json)
26 .map_err(|e| ::wasm_bindgen::JsError::new(&e))
27 }
28
29 /// Информация о загруженной карте: setId, масштабированные
30 /// респауны, размеры мира (JSON).
31 pub fn map_info(&self) -> String {
32 self.state.map_info_json()
33 }
34
35 // ***** участники ***** //
36
37 pub fn spawn_actor(
38 &mut self,
39 game_id: u32,
40 model: &str,
41 team_id: u8,
42 x: f32,
43 y: f32,
44 angle_deg: f32,
45 ) -> Result<(), ::wasm_bindgen::JsError> {
46 self.state
47 .spawn_actor(game_id, model, team_id, x, y, angle_deg)
48 .map_err(|e| ::wasm_bindgen::JsError::new(&e))
49 }
50
51 pub fn remove_actor(&mut self, game_id: u32) {
52 self.state.remove_actor(game_id);
53 }
54
55 /// Респаун/смена команды (аналог Game.changePlayerData).
56 pub fn reset_actor(
57 &mut self,
58 game_id: u32,
59 team_id: u8,
60 x: f32,
61 y: f32,
62 angle_deg: f32,
63 ) {
64 self.state.reset_actor(game_id, team_id, x, y, angle_deg);
65 }
66
67 /// Сброс здоровья/боезапаса всех танков (аналог Panel.reset).
68 pub fn reset_all_vitals(&mut self) {
69 self.state.reset_all_vitals();
70 }
71
72 pub fn spawn_scripted_actor(
73 &mut self,
74 game_id: u32,
75 model: &str,
76 team_id: u8,
77 x: f32,
78 y: f32,
79 angle_deg: f32,
80 ) -> Result<(), ::wasm_bindgen::JsError> {
81 self.state
82 .spawn_scripted_actor(game_id, model, team_id, x, y, angle_deg)
83 .map_err(|e| ::wasm_bindgen::JsError::new(&e))
84 }
85
86 pub fn remove_scripted_actor(&mut self, game_id: u32) {
87 self.state.remove_scripted_actor(game_id);
88 }
89
90 // ***** ввод и шаг ***** //
91
92 /// Ввод игрока: seq + action ('down'/'up') + имя клавиши
93 /// (wire-формат 'seq:action:name' разбирает JS-оболочка).
94 pub fn apply_input(&mut self, game_id: u32, seq: u32, action: &str, key_name: &str) {
95 self.state.apply_input(game_id, seq, action, key_name);
96 }
97
98 /// Ввод указателем: seq + мировая точка + биты состояния
99 /// (wire-формат 'seq:aim:x:y:flags' разбирает JS-оболочка).
100 pub fn apply_aim(&mut self, game_id: u32, seq: u32, x: f32, y: f32, flags: u32) {
101 self.state.apply_aim(game_id, seq, x, y, flags);
102 }
103
104 /// Шаг симуляции: фиксированные подшаги физики + ИИ ботов.
105 pub fn step(&mut self, dt: f32) {
106 self.state.step(dt);
107 }
108
109 /// События за тик (kill/health/ammo/weapon/shake) одной
110 /// JSON-строкой; буфер очищается.
111 pub fn take_events(&mut self) -> String {
112 self.state.take_events_json()
113 }
114
115 // ***** запросы состояния ***** //
116
117 pub fn last_input_seq(&self, game_id: u32) -> u32 {
118 self.state.last_input_seq(game_id)
119 }
120
121 pub fn is_alive(&self, game_id: u32) -> bool {
122 self.state.is_alive(game_id)
123 }
124
125 /// Координаты танка [x, y] (скруглены до 2 знаков) или пустой
126 /// массив.
127 pub fn position_of(&self, game_id: u32) -> Vec<f32> {
128 self.state
129 .actor_position(game_id)
130 .map(|p| p.to_vec())
131 .unwrap_or_default()
132 }
133
134 /// Полные данные всех игроков (Game.getPlayersData) одной
135 /// JSON-строкой для первого кадра (FIRST_SHOT_DATA). Не
136 /// дренирует накопители.
137 pub fn players_data(&self) -> String {
138 self.state.players_json()
139 }
140
141 /// Живые игроки плоским массивом [id, teamId, x, y, ...]
142 /// (аналог Game.getAlivePlayers для меты).
143 pub fn alive_players(&self) -> Vec<f32> {
144 self.state.alive_players_flat()
145 }
146
147 // ***** снапшот ***** //
148
149 /// Пакует broadcast-тело кадра, дренируя накопленные события
150 /// снапшота. Вызывать один раз на отправляемый кадр (throttle
151 /// частоты отправки — забота JS-оболочки).
152 pub fn pack_body(&mut self) -> Result<(), ::wasm_bindgen::JsError> {
153 let blocks = self.state.build_snapshot_blocks();
154
155 self.packer
156 .pack_body(&blocks)
157 .map_err(|e| ::wasm_bindgen::JsError::new(&e))
158 }
159
160 /// Собирает per-user кадр v3 во внутренний буфер, возвращает
161 /// длину. Кадр читается zero-copy через frame_ptr() + память
162 /// WASM. player_id < 0 — кадр без player-блока (наблюдатель).
163 #[allow(clippy::too_many_arguments)]
164 pub fn pack_frame(
165 &mut self,
166 server_time: f64,
167 seq: u32,
168 has_camera: bool,
169 camera_x: f32,
170 camera_y: f32,
171 force_reset: bool,
172 shake: Option<String>,
173 player_id: i32,
174 ) -> usize {
175 let camera = has_camera.then_some($crate::snapshot::CameraData {
176 x: camera_x,
177 y: camera_y,
178 force_reset,
179 shake,
180 });
181
182 let player = if player_id >= 0 {
183 let game_id = player_id as u32;
184
185 self.state.prediction_state(game_id).map(|(state, centering)| {
186 $crate::snapshot::PlayerBlock {
187 game_id: game_id as u8,
188 input_seq: self.state.last_input_seq(game_id),
189 state,
190 centering,
191 }
192 })
193 } else {
194 None
195 };
196
197 self.packer
198 .pack_frame(server_time, seq, camera.as_ref(), player.as_ref())
199 .len()
200 }
201
202 /// Содержал ли последний `pack_body()` событийные блоки
203 /// (трассеры/бомбы/взрывы/удаления). JS-Worker вызывает после
204 /// `pack_body()` для выбора канала WebRTC: события → meta
205 /// (reliable), только позиции → state.
206 pub fn body_has_events(&self) -> bool {
207 self.state.body_has_events()
208 }
209
210 /// Указатель на буфер последнего кадра (zero-copy чтение из JS:
211 /// new Uint8Array(wasm.memory.buffer, ptr, len)).
212 pub fn frame_ptr(&self) -> *const u8 {
213 self.packer.frame_bytes().as_ptr()
214 }
215
216 /// Копия последнего кадра (nodejs-таргет не отдаёт память
217 /// наружу; горячий путь браузера использует frame_ptr + память
218 /// WASM).
219 pub fn frame_bytes(&self) -> Vec<u8> {
220 self.packer.frame_bytes().to_vec()
221 }
222
223 // ***** очистка и handoff ***** //
224
225 /// Удаляет игроков и снаряды, возвращает JSON-массив имён для
226 /// очистки полотна клиентов (Game.removePlayersAndShots).
227 pub fn remove_players_and_shots(&mut self) -> String {
228 ::serde_json::to_string(&self.state.remove_players_and_shots())
229 .unwrap_or_else(|_| "[]".to_string())
230 }
231
232 /// Полная очистка мира (смена карты).
233 pub fn clear(&mut self) {
234 self.state.clear();
235 }
236
237 /// Курированный дамп мира для отладки (тела, коллайдеры,
238 /// карта, нав-граф, spatial-сетка, rng, аккумулятор) —
239 /// читаемая альтернатива serialize_state.
240 pub fn debug_json(&self) -> String {
241 self.state.debug_json()
242 }
243
244 /// Дамп состояния симуляции (Worker Handoff, Этап 5.2).
245 pub fn serialize_state(&self) -> Result<Vec<u8>, ::wasm_bindgen::JsError> {
246 self.state
247 .serialize_state()
248 .map_err(|e| ::wasm_bindgen::JsError::new(&e))
249 }
250
251 pub fn deserialize_state(&mut self, data: &[u8]) -> Result<(), ::wasm_bindgen::JsError> {
252 self.state
253 .deserialize_state(data)
254 .map_err(|e| ::wasm_bindgen::JsError::new(&e))
255 }
256 }
257 };
258}
259
260/// `export_client_core_abi!` — генерирует `#[wasm_bindgen] impl` для
261/// клиентского `ClientCore`, зеркально `export_game_core_abi!` (см. выше).
262/// Методы ниже — движковый минимум (§3.4 PLAN.md): 1:1-делегации в generic
263/// `vimp_engine_core::client::game::ClientState<G>`, чья сигнатура
264/// зафиксирована трейтом `GameClientDef` независимо от конкретной игры.
265///
266/// Контракт: тип-цель обязан иметь поле
267/// `state: vimp_engine_core::client::game::ClientState<G>` (под именем
268/// `ClientState<...>`, конкретный `G: GameClientDef` — забота игрового
269/// crate), и раскрываться в модуле, где уже подключён `wasm-bindgen`.
270/// Игровые методы вне минимума (`set_model`, `try_fire`, `cycle_weapon`,
271/// `sync_panel` и т.п. — сигнатуры которых по форме зависят от игры)
272/// остаются рукописными в game-crate рядом с раскрытием макроса.
273#[macro_export]
274macro_rules! export_client_core_abi {
275 ($ClientCoreTy:ty) => {
276 #[::wasm_bindgen::prelude::wasm_bindgen]
277 impl $ClientCoreTy {
278 // ***** сеть ***** //
279
280 /// Бинарный кадр из транспорта: распаковка, вставка в буфер по
281 /// seq (+дедупликация/опоздавшие), reconciliation предикта по
282 /// player-блоку. false — кадр отброшен (чужой порт/версия/
283 /// повреждён).
284 pub fn push_frame(&mut self, data: &[u8], local_now: f64) -> bool {
285 self.state.push_frame(data, local_now)
286 }
287
288 /// Свой gameId из последнего player-блока; -1, если ещё не
289 /// приходил.
290 pub fn my_game_id(&self) -> i32 {
291 self.state.my_game_id().map(|id| id as i32).unwrap_or(-1)
292 }
293
294 /// EMA-оценка (serverTime − localNow); NaN, если кадров ещё не
295 /// было. Это разница часов (`Date.now` хоста против
296 /// `performance.now` клиента), а **не** латентность: за оценку
297 /// RTT её принимать нельзя.
298 pub fn offset(&self) -> f64 {
299 self.state.offset().unwrap_or(f64::NAN)
300 }
301
302 // ***** рендер-тик ***** //
303
304 /// Весь рендер-тик: выдача пересечённых кадров (фильтр дублей
305 /// своих эффектов → JSON-очередь), интерполяция, шаг предикта,
306 /// запись hot-буфера. Возвращает длину hot-буфера в
307 /// f32-элементах.
308 pub fn sample(&mut self, local_now: f64) -> usize {
309 self.state.sample(local_now)
310 }
311
312 /// Указатель на hot-буфер (zero-copy чтение из JS:
313 /// new Float32Array(wasm.memory.buffer, ptr, len) — view
314 /// пересоздавать каждый тик, рост памяти WASM инвалидирует
315 /// buffer).
316 pub fn hot_ptr(&self) -> *const f32 {
317 self.state.hot().as_ptr()
318 }
319
320 /// Копия hot-буфера (nodejs-таргет; горячий путь браузера —
321 /// hot_ptr).
322 pub fn hot_values(&self) -> Vec<f32> {
323 self.state.hot().to_vec()
324 }
325
326 /// Событийные кадры JSON-строкой [{game, camera}, ...] в
327 /// форме, готовой для applyShot; вызывать при флаге hasFrames
328 /// hot-буфера, очередь очищается.
329 pub fn take_frames(&mut self) -> String {
330 self.state.take_frames()
331 }
332
333 // ***** ввод ***** //
334
335 /// Ввод игрока: action ('down'/'up') + имя клавиши — в историю
336 /// предикта.
337 pub fn apply_input(&mut self, action: &str, key_name: &str, local_now: f64) {
338 self.state.apply_input(action, key_name, local_now);
339 }
340
341 /// Ввод указателем: мировая точка + биты состояния — в историю
342 /// предикта.
343 pub fn apply_aim(&mut self, x: f32, y: f32, flags: u32, local_now: f64) {
344 self.state.apply_aim(x, y, flags, local_now);
345 }
346
347 // ***** жизненный цикл ***** //
348
349 /// Смена режима игрок/спектатор (KEYSET_DATA).
350 pub fn set_active(&mut self, active: bool) {
351 self.state.set_active(active);
352 }
353
354 /// Данные карты (MAP_DATA): мир raycast + сброс буфера и
355 /// предикта.
356 pub fn set_map(&mut self, map_json: &str) -> Result<(), ::wasm_bindgen::JsError> {
357 self.state
358 .set_map(map_json)
359 .map_err(|e| ::wasm_bindgen::JsError::new(&e))
360 }
361
362 /// Полный сброс (порт CLEAR).
363 pub fn reset(&mut self) {
364 self.state.reset();
365 }
366
367 /// Ресинк часов после долгой паузы вкладки: сброс сетевого
368 /// буфера и очереди кадров без обнуления предикта.
369 pub fn resync(&mut self) {
370 self.state.resync();
371 }
372
373 // ***** тесты и харнесс ***** //
374
375 /// Дамп клиентского состояния для отладки: сетевой буфер
376 /// (глубина, окно seq, оффсет, последний кадр), свой gameId,
377 /// размеры hot-буфера и очереди событийных кадров.
378 pub fn debug_json(&self) -> String {
379 self.state.debug_json()
380 }
381
382 /// Записи расхождения предикта с авторитетным состоянием
383 /// (JSON {samples, violations, dropped, maxDelta, records});
384 /// очередь очищается. 'null' — детектор выключен (конфиг без
385 /// секции divergence, боевой путь).
386 pub fn take_divergence(&mut self) -> String {
387 self.state.take_divergence()
388 }
389
390 /// Чистая распаковка кадра v3 → JSON {port, seq, serverTime,
391 /// camera, player, snapshot} (замена unpackFrame в тестах);
392 /// 'null' при несовпадении версии или повреждённом кадре.
393 pub fn decode_frame(&self, data: &[u8]) -> String {
394 self.state.decode_frame(data)
395 }
396 }
397 };
398}