Skip to main content

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            /// Шаг симуляции: фиксированные подшаги физики + ИИ ботов.
99            pub fn step(&mut self, dt: f32) {
100                self.state.step(dt);
101            }
102
103            /// События за тик (kill/health/ammo/weapon/shake) одной
104            /// JSON-строкой; буфер очищается.
105            pub fn take_events(&mut self) -> String {
106                self.state.take_events_json()
107            }
108
109            // ***** запросы состояния ***** //
110
111            pub fn last_input_seq(&self, game_id: u32) -> u32 {
112                self.state.last_input_seq(game_id)
113            }
114
115            pub fn is_alive(&self, game_id: u32) -> bool {
116                self.state.is_alive(game_id)
117            }
118
119            /// Координаты танка [x, y] (скруглены до 2 знаков) или пустой
120            /// массив.
121            pub fn position_of(&self, game_id: u32) -> Vec<f32> {
122                self.state
123                    .actor_position(game_id)
124                    .map(|p| p.to_vec())
125                    .unwrap_or_default()
126            }
127
128            /// Полные данные всех игроков (Game.getPlayersData) одной
129            /// JSON-строкой для первого кадра (FIRST_SHOT_DATA). Не
130            /// дренирует накопители.
131            pub fn players_data(&self) -> String {
132                self.state.players_json()
133            }
134
135            /// Живые игроки плоским массивом [id, teamId, x, y, ...]
136            /// (аналог Game.getAlivePlayers для меты).
137            pub fn alive_players(&self) -> Vec<f32> {
138                self.state.alive_players_flat()
139            }
140
141            // ***** снапшот ***** //
142
143            /// Пакует broadcast-тело кадра, дренируя накопленные события
144            /// снапшота. Вызывать один раз на отправляемый кадр (throttle
145            /// частоты отправки — забота JS-оболочки).
146            pub fn pack_body(&mut self) -> Result<(), ::wasm_bindgen::JsError> {
147                let blocks = self.state.build_snapshot_blocks();
148
149                self.packer
150                    .pack_body(&blocks)
151                    .map_err(|e| ::wasm_bindgen::JsError::new(&e))
152            }
153
154            /// Собирает per-user кадр v3 во внутренний буфер, возвращает
155            /// длину. Кадр читается zero-copy через frame_ptr() + память
156            /// WASM. player_id < 0 — кадр без player-блока (наблюдатель).
157            #[allow(clippy::too_many_arguments)]
158            pub fn pack_frame(
159                &mut self,
160                server_time: f64,
161                seq: u32,
162                has_camera: bool,
163                camera_x: f32,
164                camera_y: f32,
165                force_reset: bool,
166                shake: Option<String>,
167                player_id: i32,
168            ) -> usize {
169                let camera = has_camera.then_some($crate::snapshot::CameraData {
170                    x: camera_x,
171                    y: camera_y,
172                    force_reset,
173                    shake,
174                });
175
176                let player = if player_id >= 0 {
177                    let game_id = player_id as u32;
178
179                    self.state.prediction_state(game_id).map(|(state, centering)| {
180                        $crate::snapshot::PlayerBlock {
181                            game_id: game_id as u8,
182                            input_seq: self.state.last_input_seq(game_id),
183                            state,
184                            centering,
185                        }
186                    })
187                } else {
188                    None
189                };
190
191                self.packer
192                    .pack_frame(server_time, seq, camera.as_ref(), player.as_ref())
193                    .len()
194            }
195
196            /// Содержал ли последний `pack_body()` событийные блоки
197            /// (трассеры/бомбы/взрывы/удаления). JS-Worker вызывает после
198            /// `pack_body()` для выбора канала WebRTC: события → meta
199            /// (reliable), только позиции → state.
200            pub fn body_has_events(&self) -> bool {
201                self.state.body_has_events()
202            }
203
204            /// Указатель на буфер последнего кадра (zero-copy чтение из JS:
205            /// new Uint8Array(wasm.memory.buffer, ptr, len)).
206            pub fn frame_ptr(&self) -> *const u8 {
207                self.packer.frame_bytes().as_ptr()
208            }
209
210            /// Копия последнего кадра (nodejs-таргет не отдаёт память
211            /// наружу; горячий путь браузера использует frame_ptr + память
212            /// WASM).
213            pub fn frame_bytes(&self) -> Vec<u8> {
214                self.packer.frame_bytes().to_vec()
215            }
216
217            // ***** очистка и handoff ***** //
218
219            /// Удаляет игроков и снаряды, возвращает JSON-массив имён для
220            /// очистки полотна клиентов (Game.removePlayersAndShots).
221            pub fn remove_players_and_shots(&mut self) -> String {
222                ::serde_json::to_string(&self.state.remove_players_and_shots())
223                    .unwrap_or_else(|_| "[]".to_string())
224            }
225
226            /// Полная очистка мира (смена карты).
227            pub fn clear(&mut self) {
228                self.state.clear();
229            }
230
231            /// Курированный дамп мира для отладки (тела, коллайдеры,
232            /// карта, нав-граф, spatial-сетка, rng, аккумулятор) —
233            /// читаемая альтернатива serialize_state.
234            pub fn debug_json(&self) -> String {
235                self.state.debug_json()
236            }
237
238            /// Дамп состояния симуляции (Worker Handoff, Этап 5.2).
239            pub fn serialize_state(&self) -> Result<Vec<u8>, ::wasm_bindgen::JsError> {
240                self.state
241                    .serialize_state()
242                    .map_err(|e| ::wasm_bindgen::JsError::new(&e))
243            }
244
245            pub fn deserialize_state(&mut self, data: &[u8]) -> Result<(), ::wasm_bindgen::JsError> {
246                self.state
247                    .deserialize_state(data)
248                    .map_err(|e| ::wasm_bindgen::JsError::new(&e))
249            }
250        }
251    };
252}
253
254/// `export_client_core_abi!` — генерирует `#[wasm_bindgen] impl` для
255/// клиентского `ClientCore`, зеркально `export_game_core_abi!` (см. выше).
256/// Методы ниже — движковый минимум (§3.4 PLAN.md): 1:1-делегации в generic
257/// `vimp_engine_core::client::game::ClientState<G>`, чья сигнатура
258/// зафиксирована трейтом `GameClientDef` независимо от конкретной игры.
259///
260/// Контракт: тип-цель обязан иметь поле
261/// `state: vimp_engine_core::client::game::ClientState<G>` (под именем
262/// `ClientState<...>`, конкретный `G: GameClientDef` — забота игрового
263/// crate), и раскрываться в модуле, где уже подключён `wasm-bindgen`.
264/// Игровые методы вне минимума (`set_model`, `try_fire`, `cycle_weapon`,
265/// `sync_panel` и т.п. — сигнатуры которых по форме зависят от игры)
266/// остаются рукописными в game-crate рядом с раскрытием макроса.
267#[macro_export]
268macro_rules! export_client_core_abi {
269    ($ClientCoreTy:ty) => {
270        #[::wasm_bindgen::prelude::wasm_bindgen]
271        impl $ClientCoreTy {
272            // ***** сеть ***** //
273
274            /// Бинарный кадр из транспорта: распаковка, вставка в буфер по
275            /// seq (+дедупликация/опоздавшие), reconciliation предикта по
276            /// player-блоку. false — кадр отброшен (чужой порт/версия/
277            /// повреждён).
278            pub fn push_frame(&mut self, data: &[u8], local_now: f64) -> bool {
279                self.state.push_frame(data, local_now)
280            }
281
282            /// Свой gameId из последнего player-блока; -1, если ещё не
283            /// приходил.
284            pub fn my_game_id(&self) -> i32 {
285                self.state.my_game_id().map(|id| id as i32).unwrap_or(-1)
286            }
287
288            /// EMA-оценка (serverTime − localNow); NaN, если кадров ещё не
289            /// было. Это разница часов (`Date.now` хоста против
290            /// `performance.now` клиента), а **не** латентность: за оценку
291            /// RTT её принимать нельзя.
292            pub fn offset(&self) -> f64 {
293                self.state.offset().unwrap_or(f64::NAN)
294            }
295
296            // ***** рендер-тик ***** //
297
298            /// Весь рендер-тик: выдача пересечённых кадров (фильтр дублей
299            /// своих эффектов → JSON-очередь), интерполяция, шаг предикта,
300            /// запись hot-буфера. Возвращает длину hot-буфера в
301            /// f32-элементах.
302            pub fn sample(&mut self, local_now: f64) -> usize {
303                self.state.sample(local_now)
304            }
305
306            /// Указатель на hot-буфер (zero-copy чтение из JS:
307            /// new Float32Array(wasm.memory.buffer, ptr, len) — view
308            /// пересоздавать каждый тик, рост памяти WASM инвалидирует
309            /// buffer).
310            pub fn hot_ptr(&self) -> *const f32 {
311                self.state.hot().as_ptr()
312            }
313
314            /// Копия hot-буфера (nodejs-таргет; горячий путь браузера —
315            /// hot_ptr).
316            pub fn hot_values(&self) -> Vec<f32> {
317                self.state.hot().to_vec()
318            }
319
320            /// Событийные кадры JSON-строкой [{game, camera}, ...] в
321            /// форме, готовой для applyShot; вызывать при флаге hasFrames
322            /// hot-буфера, очередь очищается.
323            pub fn take_frames(&mut self) -> String {
324                self.state.take_frames()
325            }
326
327            // ***** ввод ***** //
328
329            /// Ввод игрока: action ('down'/'up') + имя клавиши — в историю
330            /// предикта.
331            pub fn apply_input(&mut self, action: &str, key_name: &str, local_now: f64) {
332                self.state.apply_input(action, key_name, local_now);
333            }
334
335            // ***** жизненный цикл ***** //
336
337            /// Смена режима игрок/спектатор (KEYSET_DATA).
338            pub fn set_active(&mut self, active: bool) {
339                self.state.set_active(active);
340            }
341
342            /// Данные карты (MAP_DATA): мир raycast + сброс буфера и
343            /// предикта.
344            pub fn set_map(&mut self, map_json: &str) -> Result<(), ::wasm_bindgen::JsError> {
345                self.state
346                    .set_map(map_json)
347                    .map_err(|e| ::wasm_bindgen::JsError::new(&e))
348            }
349
350            /// Полный сброс (порт CLEAR).
351            pub fn reset(&mut self) {
352                self.state.reset();
353            }
354
355            /// Ресинк часов после долгой паузы вкладки: сброс сетевого
356            /// буфера и очереди кадров без обнуления предикта.
357            pub fn resync(&mut self) {
358                self.state.resync();
359            }
360
361            // ***** тесты и харнесс ***** //
362
363            /// Дамп клиентского состояния для отладки: сетевой буфер
364            /// (глубина, окно seq, оффсет, последний кадр), свой gameId,
365            /// размеры hot-буфера и очереди событийных кадров.
366            pub fn debug_json(&self) -> String {
367                self.state.debug_json()
368            }
369
370            /// Записи расхождения предикта с авторитетным состоянием
371            /// (JSON {samples, violations, dropped, maxDelta, records});
372            /// очередь очищается. 'null' — детектор выключен (конфиг без
373            /// секции divergence, боевой путь).
374            pub fn take_divergence(&mut self) -> String {
375                self.state.take_divergence()
376            }
377
378            /// Чистая распаковка кадра v3 → JSON {port, seq, serverTime,
379            /// camera, player, snapshot} (замена unpackFrame в тестах);
380            /// 'null' при несовпадении версии или повреждённом кадре.
381            pub fn decode_frame(&self, data: &[u8]) -> String {
382                self.state.decode_frame(data)
383            }
384        }
385    };
386}