vimp-engine 0.22.1 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/bin/vimp-surface.js +88 -0
  2. package/core/Cargo.toml +1 -1
  3. package/package.json +1 -1
  4. package/src/client/lib/formBuilder.js +38 -27
  5. package/src/client/lib/pickActiveGame.js +59 -0
  6. package/src/client/lib/socketDispatch.js +34 -0
  7. package/src/client/main.js +61 -14
  8. package/src/config/abiOps.js +37 -0
  9. package/src/config/clientServices.js +27 -0
  10. package/src/config/opcodes.js +11 -2
  11. package/src/config/wsports.js +9 -0
  12. package/src/devtools/contract/loadContext.js +25 -2
  13. package/src/devtools/contract/rules/b10-respawns.js +5 -3
  14. package/src/devtools/contract/rules/b2-engine-api.js +81 -10
  15. package/src/devtools/contract/rules/b3-game-config-shape.js +46 -5
  16. package/src/devtools/contract/rules/b4-teams.js +6 -3
  17. package/src/devtools/contract/rules/b5-room-form.js +40 -8
  18. package/src/devtools/contract/rules/c10-auth-schema.js +34 -1
  19. package/src/devtools/contract/rules/c4-component-dependencies.js +10 -15
  20. package/src/devtools/surface/abiParse.js +175 -0
  21. package/src/devtools/surface/collect.js +413 -0
  22. package/src/host/GameCoreAdapter.js +49 -0
  23. package/src/lib/applyRoomOverrides.js +16 -2
  24. package/src/lib/capabilities.js +32 -0
  25. package/src/lib/coreAbi.js +116 -0
  26. package/src/lib/coreConfig.js +3 -3
  27. package/src/lib/createHostRuntime.js +5 -3
  28. package/src/lib/formControls.js +89 -0
  29. package/src/lib/formUnit.js +25 -0
  30. package/src/lib/gameConfigView.js +214 -0
  31. package/src/lib/gamePlugin.js +86 -86
  32. package/src/lib/loadGamePackage.js +9 -2
  33. package/src/lib/registry.js +94 -0
  34. package/src/lib/validators.js +79 -17
  35. package/src/standalone/index.js +42 -13
  36. package/tests/fixtures/generations/gen-api3/README.md +7 -0
  37. package/tests/fixtures/generations/gen-api3/client/fakeClientCore.js +307 -0
  38. package/tests/fixtures/generations/gen-api3/client/index.js +38 -0
  39. package/tests/fixtures/generations/gen-api3/client/parts/Actor.js +18 -0
  40. package/tests/fixtures/generations/gen-api3/client/parts/ActorRadar.js +7 -0
  41. package/tests/fixtures/generations/gen-api3/config/auth.js +41 -0
  42. package/tests/fixtures/generations/gen-api3/config/client.js +151 -0
  43. package/tests/fixtures/generations/gen-api3/config/game.js +167 -0
  44. package/tests/fixtures/generations/gen-api3/core/pkg-node/core.js +4 -0
  45. package/tests/fixtures/generations/gen-api3/host/ScriptedManager.js +129 -0
  46. package/tests/fixtures/generations/gen-api3/host/createModules.js +6 -0
  47. package/tests/fixtures/generations/gen-api3/host/fakeCore.js +263 -0
  48. package/tests/fixtures/generations/gen-api3/host/index.js +30 -0
  49. package/tests/fixtures/generations/gen-api3/host/spawnCommand.js +14 -0
  50. package/tests/fixtures/generations/gen-api3/host/systemMessages.js +6 -0
  51. package/tests/fixtures/generations/gen-api3/manifest.json +25 -0
  52. package/tests/fixtures/generations/gen-api4/README.md +7 -0
  53. package/tests/fixtures/generations/gen-api4/client/fakeClientCore.js +307 -0
  54. package/tests/fixtures/generations/gen-api4/client/index.js +38 -0
  55. package/tests/fixtures/generations/gen-api4/client/parts/Actor.js +18 -0
  56. package/tests/fixtures/generations/gen-api4/client/parts/ActorRadar.js +7 -0
  57. package/tests/fixtures/generations/gen-api4/config/auth.js +41 -0
  58. package/tests/fixtures/generations/gen-api4/config/client.js +151 -0
  59. package/tests/fixtures/generations/gen-api4/config/game.js +156 -0
  60. package/tests/fixtures/generations/gen-api4/core/pkg-node/core.js +4 -0
  61. package/tests/fixtures/generations/gen-api4/host/ScriptedManager.js +129 -0
  62. package/tests/fixtures/generations/gen-api4/host/createModules.js +6 -0
  63. package/tests/fixtures/generations/gen-api4/host/fakeCore.js +263 -0
  64. package/tests/fixtures/generations/gen-api4/host/index.js +30 -0
  65. package/tests/fixtures/generations/gen-api4/host/spawnCommand.js +14 -0
  66. package/tests/fixtures/generations/gen-api4/host/systemMessages.js +6 -0
  67. package/tests/fixtures/generations/gen-api4/manifest.json +17 -0
  68. package/tests/fixtures/miniGame/client/fakeClientCore.js +17 -0
  69. package/tests/fixtures/miniGame/host/fakeCore.js +38 -8
@@ -0,0 +1,263 @@
1
+ // Фейковое JS-ядро миниигры-фикстуры (Этап 7 плана отделения движка,
2
+ // PLAN.md §4/Wasm Host ABI §3.4): реализует полную поверхность методов,
3
+ // которую вызывает GameCoreAdapter, без Rust/WASM — доказательство, что
4
+ // движок не завязан на конкретную реализацию ядра. Актёры — плоские
5
+ // объекты { x, y, angle, team, alive }; тик — тривиальная линейная
6
+ // интеграция скорости (та же идея, что у Rust-фикстуры TestClient,
7
+ // core/src/client/game.rs: форма ABI важнее физики).
8
+ export default class FakeGameCore {
9
+ constructor(configJson) {
10
+ this._config = JSON.parse(configJson);
11
+ this._actors = new Map(); // gameId -> { x, y, angle, team, alive, vx, vy, lastInputSeq }
12
+ this._map = null;
13
+ this._events = [];
14
+ this._lastBody = [];
15
+ this._lastFrame = null;
16
+ }
17
+
18
+ // ***** карта ***** //
19
+
20
+ load_map(mapJson) {
21
+ this._map = JSON.parse(mapJson);
22
+ }
23
+
24
+ map_info() {
25
+ if (!this._map) {
26
+ return 'null';
27
+ }
28
+
29
+ return JSON.stringify({
30
+ setId: this._map.setId,
31
+ respawns: this._map.respawns || {},
32
+ });
33
+ }
34
+
35
+ clear() {
36
+ this._actors.clear();
37
+ this._events.length = 0;
38
+ this._lastBody = [];
39
+ }
40
+
41
+ // ***** участники ***** //
42
+
43
+ spawn_actor(gameId, model, teamId, x, y, angle) {
44
+ this._actors.set(gameId, {
45
+ x,
46
+ y,
47
+ angle,
48
+ team: teamId,
49
+ alive: true,
50
+ vx: 0,
51
+ vy: 0,
52
+ lastInputSeq: 0,
53
+ });
54
+
55
+ this._events.push({ type: 'panelSet', id: gameId, field: 'energy', value: 100 });
56
+ }
57
+
58
+ remove_actor(gameId) {
59
+ this._actors.delete(gameId);
60
+ }
61
+
62
+ reset_actor(gameId, teamId, x, y, angle) {
63
+ const actor = this._actors.get(gameId);
64
+
65
+ if (actor) {
66
+ Object.assign(actor, { x, y, angle, team: teamId, alive: true, vx: 0, vy: 0 });
67
+ }
68
+ }
69
+
70
+ reset_all_vitals() {
71
+ for (const actor of this._actors.values()) {
72
+ actor.alive = true;
73
+ }
74
+ }
75
+
76
+ spawn_scripted_actor(gameId, model, teamId, x, y, angle) {
77
+ this.spawn_actor(gameId, model, teamId, x, y, angle);
78
+ this._actors.get(gameId).scripted = true;
79
+ }
80
+
81
+ remove_scripted_actor(gameId) {
82
+ this.remove_actor(gameId);
83
+ }
84
+
85
+ remove_players_and_shots() {
86
+ const names = [...this._actors.keys()].map(String);
87
+
88
+ this._actors.clear();
89
+
90
+ return JSON.stringify(names);
91
+ }
92
+
93
+ // ***** ввод ***** //
94
+
95
+ apply_input(gameId, seq, action, name) {
96
+ const actor = this._actors.get(gameId);
97
+
98
+ if (!actor) {
99
+ return;
100
+ }
101
+
102
+ actor.lastInputSeq = seq;
103
+
104
+ const magnitude = action === 'down' ? 40 : 0;
105
+
106
+ if (name === 'forward') {
107
+ actor.vy = -magnitude;
108
+ } else if (name === 'back') {
109
+ actor.vy = magnitude;
110
+ } else if (name === 'fire' && action === 'down') {
111
+ this._events.push({ type: 'custom', data: { kind: 'fire', id: gameId } });
112
+ }
113
+ }
114
+
115
+ // ввод указателем: миниигра просто запоминает последнюю точку и биты —
116
+ // движковым тестам важно, что канал доходит до ядра
117
+ apply_aim(gameId, seq, x, y, flags) {
118
+ const actor = this._actors.get(gameId);
119
+
120
+ if (!actor) {
121
+ return;
122
+ }
123
+
124
+ actor.lastInputSeq = seq;
125
+ actor.aim = { x, y, flags };
126
+ }
127
+
128
+ aim_of(gameId) {
129
+ return this._actors.get(gameId)?.aim ?? null;
130
+ }
131
+
132
+ last_input_seq(gameId) {
133
+ return this._actors.get(gameId)?.lastInputSeq ?? 0;
134
+ }
135
+
136
+ // ***** запросы состояния ***** //
137
+
138
+ is_alive(gameId) {
139
+ return Boolean(this._actors.get(gameId)?.alive);
140
+ }
141
+
142
+ position_of(gameId) {
143
+ const actor = this._actors.get(gameId);
144
+
145
+ return actor ? [actor.x, actor.y] : [];
146
+ }
147
+
148
+ // FIRST_SHOT_DATA: полный снапшот игроков в раскладке снапшот-схемы
149
+ // (ключ схемы -> id -> поля), а не плоским списком: ровно её разбирает
150
+ // клиент (applyShot браузера и VirtualClient._applyGameData). Плоский
151
+ // список доезжал до клиента ключами '0', '1' — сущность первого кадра
152
+ // не появлялась на холсте, и видно это только со второго участника
153
+ players_data() {
154
+ const entry = Object.entries(
155
+ this._config.engine?.snapshot?.keys ?? {},
156
+ ).find(([, spec]) => spec.class === 'hot' && spec.kind === 'indexed8');
157
+
158
+ if (!entry) {
159
+ return '{}';
160
+ }
161
+
162
+ const [key, spec] = entry;
163
+ const rows = {};
164
+
165
+ for (const [id, actor] of this._actors) {
166
+ rows[id] = spec.fields.map(field => Number(actor[field.name] ?? 0));
167
+ }
168
+
169
+ return JSON.stringify({ [key]: rows });
170
+ }
171
+
172
+ // ***** игровой тик ***** //
173
+
174
+ step(dt) {
175
+ for (const actor of this._actors.values()) {
176
+ actor.x += actor.vx * dt;
177
+ actor.y += actor.vy * dt;
178
+ }
179
+ }
180
+
181
+ take_events() {
182
+ const events = this._events;
183
+
184
+ this._events = [];
185
+
186
+ return JSON.stringify(events);
187
+ }
188
+
189
+ // ***** упаковка снапшота (фикстурный формат — не бинарный кодек ядра;
190
+ // достаточно для тестов меты, которые не декодируют реальный фрейминг) ***** //
191
+
192
+ // поля записи — по снапшот-схеме фикстуры (config/game.js, ключ a1):
193
+ // x, y, angle, team; клиентская половина фикстуры раскладывает их в
194
+ // hot-буфер той же ширины, что заявлена схемой
195
+ pack_body() {
196
+ this._lastBody = [...this._actors.entries()].map(([id, a]) => ({
197
+ id: Number(id),
198
+ x: a.x,
199
+ y: a.y,
200
+ angle: a.angle,
201
+ team: a.team,
202
+ }));
203
+ }
204
+
205
+ body_has_events() {
206
+ return false;
207
+ }
208
+
209
+ pack_frame(serverTime, seq, hasCamera, camX, camY, forceReset, shake, playerId) {
210
+ this._lastFrame = {
211
+ serverTime,
212
+ seq,
213
+ camera: hasCamera ? [camX, camY, Boolean(forceReset), shake ?? null] : null,
214
+ playerId,
215
+ body: this._lastBody,
216
+ };
217
+ }
218
+
219
+ frame_bytes() {
220
+ return new TextEncoder().encode(JSON.stringify(this._lastFrame));
221
+ }
222
+
223
+ // ***** отладка ***** //
224
+
225
+ // форма дампа — та же, что у движкового crate::debug (тела/карта/rng),
226
+ // с поправкой на фикстуру без физики: актёры и есть тела
227
+ debug_json() {
228
+ return JSON.stringify({
229
+ bodies: [...this._actors.entries()].map(([id, a]) => ({
230
+ handle: Number(id),
231
+ tag: 2,
232
+ userData: String(id),
233
+ translation: [a.x, a.y],
234
+ rotation: a.angle,
235
+ linvel: [a.vx, a.vy],
236
+ angvel: 0,
237
+ mass: 1,
238
+ bodyType: 'Dynamic',
239
+ ccd: false,
240
+ })),
241
+ colliders: [],
242
+ map: this._map
243
+ ? { setId: this._map.setId, step: this._map.step ?? null }
244
+ : null,
245
+ nav: null,
246
+ spatial: { cells: 0, entities: 0, cellCounts: [] },
247
+ rng: { state: '0' },
248
+ step: { timeStep: this._config.timeStep ?? null, accumulator: 0 },
249
+ });
250
+ }
251
+
252
+ // ***** handoff (задел, PLAN.md §6 «открытые вопросы») ***** //
253
+
254
+ serialize_state() {
255
+ return new TextEncoder().encode(JSON.stringify([...this._actors.entries()]));
256
+ }
257
+
258
+ deserialize_state(bytes) {
259
+ const entries = JSON.parse(new TextDecoder().decode(bytes));
260
+
261
+ this._actors = new Map(entries);
262
+ }
263
+ }
@@ -0,0 +1,30 @@
1
+ // ЗАМОРОЖЕНО. Снимок плагина 2026-08-29 (поколение engineApi: 4).
2
+ // Не править: правка фикстуры маскирует слом совместимости.
3
+ import FakeGameCore from './fakeCore.js';
4
+ import gameConfig from '../config/game.js';
5
+ import authSchema from '../config/auth.js';
6
+ import clientConfig from '../config/client.js';
7
+ import systemMessages from './systemMessages.js';
8
+ import spawnCommand from './spawnCommand.js';
9
+ import createModules from './createModules.js';
10
+
11
+ // HostPlugin миниигры-фикстуры (Этап 7 плана отделения движка, PLAN.md
12
+ // §3.2): доказывает, что HostGame и движковая мета работают с любым
13
+ // HostPlugin, реализующим контракт — не только с @vimp-games/tanks. createCore
14
+ // не грузит WASM (fake-core — обычный JS-класс), поэтому фикстура не
15
+ // требует собранного Rust-ядра игры.
16
+ export default {
17
+ id: 'gen-api4',
18
+ engineApi: 4,
19
+
20
+ async createCore(coreConfigJson) {
21
+ return new FakeGameCore(coreConfigJson);
22
+ },
23
+
24
+ gameConfig,
25
+ authSchema,
26
+ chatCommands: [spawnCommand],
27
+ systemMessages,
28
+ createModules,
29
+ buildClientGameConfig: () => clientConfig,
30
+ };
@@ -0,0 +1,14 @@
1
+ // Игровая чат-команда фикстуры: '/spawn <count>' — создаёт scripted-
2
+ // участников без голосования (зеркало vimp-tanks/src/host/botCommand.js,
3
+ // упрощённое — доказывает только регистрацию HostPlugin.chatCommands).
4
+ // Регистрируется в движковом CommandProcessor.
5
+ export default {
6
+ name: '/spawn',
7
+ handler(ctx, gameId, args) {
8
+ const count = Number(args[0]) || 1;
9
+ const created = ctx.scripted.createScripted(count);
10
+
11
+ ctx.chat.pushSystem('SCRIPTED_SPAWNED', [created]);
12
+ ctx.roundManager.initiateNewRound();
13
+ },
14
+ };
@@ -0,0 +1,6 @@
1
+ // Игровые коды системных сообщений фикстуры (группа g:*) — зеркало
2
+ // vimp-tanks/src/host/systemMessages.js (группа b:*). Merge в движковый
3
+ // реестр через registerCodes (HostGame конструктор).
4
+ export default {
5
+ SCRIPTED_SPAWNED: 'g:0', // {0} scripted participant(s) spawned
6
+ };
@@ -0,0 +1,17 @@
1
+ {
2
+ "id": "gen-api4",
3
+ "engineApi": 4,
4
+ "assetsBase": "",
5
+ "entries": {
6
+ "host": "host/index.js",
7
+ "client": "client/index.js",
8
+ "wasm": "core/pkg-web/core_bg.wasm",
9
+ "wasmNode": "core/pkg-node/core.js"
10
+ },
11
+ "roomDefaults": {
12
+ "maxPlayers": 4
13
+ },
14
+ "maps": [
15
+ "arena"
16
+ ]
17
+ }
@@ -301,4 +301,21 @@ export default class FakeClientCore {
301
301
  return 'null';
302
302
  }
303
303
  }
304
+
305
+ // ***** расширение (этап 4 плана plugin-forward-compat) ***** //
306
+
307
+ // самоописание: фикстура представляет ядро текущего поколения, поэтому
308
+ // отвечает так же, как раскрытие движкового макроса
309
+ abi_describe() {
310
+ return JSON.stringify({ abi: 1, core: 'fixture', ops: ['debug.json'] });
311
+ }
312
+
313
+ // опкод вместо нового символа: пустой ответ — «не обработан»
314
+ dispatch(op) {
315
+ if (op === 'debug.json') {
316
+ return new TextEncoder().encode(this.debug_json());
317
+ }
318
+
319
+ return new Uint8Array(0);
320
+ }
304
321
  }
@@ -145,15 +145,28 @@ export default class FakeGameCore {
145
145
  return actor ? [actor.x, actor.y] : [];
146
146
  }
147
147
 
148
+ // FIRST_SHOT_DATA: полный снапшот игроков в раскладке снапшот-схемы
149
+ // (ключ схемы -> id -> поля), а не плоским списком: ровно её разбирает
150
+ // клиент (applyShot браузера и VirtualClient._applyGameData). Плоский
151
+ // список доезжал до клиента ключами '0', '1' — сущность первого кадра
152
+ // не появлялась на холсте, и видно это только со второго участника
148
153
  players_data() {
149
- return JSON.stringify(
150
- [...this._actors.entries()].map(([id, a]) => ({
151
- id: Number(id),
152
- x: a.x,
153
- y: a.y,
154
- team: a.team,
155
- })),
156
- );
154
+ const entry = Object.entries(
155
+ this._config.engine?.snapshot?.keys ?? {},
156
+ ).find(([, spec]) => spec.class === 'hot' && spec.kind === 'indexed8');
157
+
158
+ if (!entry) {
159
+ return '{}';
160
+ }
161
+
162
+ const [key, spec] = entry;
163
+ const rows = {};
164
+
165
+ for (const [id, actor] of this._actors) {
166
+ rows[id] = spec.fields.map(field => Number(actor[field.name] ?? 0));
167
+ }
168
+
169
+ return JSON.stringify({ [key]: rows });
157
170
  }
158
171
 
159
172
  // ***** игровой тик ***** //
@@ -247,4 +260,21 @@ export default class FakeGameCore {
247
260
 
248
261
  this._actors = new Map(entries);
249
262
  }
263
+
264
+ // ***** расширение (этап 4 плана plugin-forward-compat) ***** //
265
+
266
+ // самоописание: фикстура представляет ядро текущего поколения, поэтому
267
+ // отвечает так же, как раскрытие движкового макроса
268
+ abi_describe() {
269
+ return JSON.stringify({ abi: 1, core: 'fixture', ops: ['debug.json'] });
270
+ }
271
+
272
+ // опкод вместо нового символа: пустой ответ — «не обработан»
273
+ dispatch(op) {
274
+ if (op === 'debug.json') {
275
+ return new TextEncoder().encode(this.debug_json());
276
+ }
277
+
278
+ return new Uint8Array(0);
279
+ }
250
280
  }