vimp-engine 0.33.2 → 0.34.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.
package/core/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "vimp-engine-core"
3
- version = "0.19.0"
3
+ version = "0.20.0"
4
4
  edition = "2024"
5
5
  description = "VIMP — движковый каркас симуляции (физика, карта, снапшот-фрейминг, интерполяция/предикт/raycast-примитивы, нав-утилиты). Без wasm-bindgen — обёртки для WASM-ABI собирает game-crate."
6
6
  license = "MIT"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vimp-engine",
3
- "version": "0.33.2",
3
+ "version": "0.34.0",
4
4
  "description": "VIMP — движок-приложение (мастер, P2P-транспорт, Worker-хост, мета, MVC-каркас клиента)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -596,6 +596,8 @@ function applyMapData(data, { notifyHost = true } = {}) {
596
596
  // значит и предсказание подъёма. Не доехав до клиентской `MapLevels`,
597
597
  // она дала бы другой уклон, чем у хоста, — расхождение молчаливое
598
598
  levelHeight: data.levelHeight,
599
+ // непрозрачные данные игры: движок их не читает и не масштабирует
600
+ game: data.game,
599
601
  }),
600
602
  );
601
603
  } catch (e) {
@@ -673,6 +675,9 @@ function applyMapData(data, { notifyHost = true } = {}) {
673
675
  // прежнее имя оставлено для парта, который его уже читает
674
676
  physicsStatic,
675
677
  scale,
678
+ // непрозрачные данные игры (`map.game`) как объявлены в карте: без
679
+ // масштабирования. `physicsDynamic[i].game` доезжает до `d{i}` сам
680
+ game: data.game,
676
681
  };
677
682
 
678
683
  staticIndex += 1;
@@ -206,6 +206,8 @@ class VirtualClient {
206
206
  // разъедется с авторитетным молча
207
207
  levels: data.levels,
208
208
  ramps: data.ramps,
209
+ // непрозрачные данные игры — как в main.js
210
+ game: data.game,
209
211
  }),
210
212
  );
211
213
  } catch (e) {
@@ -3,11 +3,16 @@ import { ERROR, skip, verdict } from '../result.js';
3
3
  // Интерполируются только f32 и только в горячих блоках. interp на u8 или
4
4
  // в event-блоке не ошибка сборки — он просто не исполняется, и поле
5
5
  // дёргается вместо плавного движения.
6
+ //
7
+ // Там же — роль `state` (байт состояния тела карты): u8 сразу за головой
8
+ // строки динамики (позиция 5 у слоёной строки, 3 — у плоской) и до
9
+ // опционального хвоста. Ядро отвергает иное при загрузке карты, то есть
10
+ // уже в запущенном матче (зеркало `BlockSchema::validate_roles`).
6
11
  export default {
7
12
  id: 'D3',
8
13
  name: 'snapshotInterp',
9
14
  level: ERROR,
10
- title: "interp only on f32 fields of class 'hot' blocks",
15
+ title: "interp only on f32 fields of class 'hot' blocks; role 'state' is a u8 right after the row head",
11
16
 
12
17
  check(ctx) {
13
18
  const snapshot = ctx.gameConfig?.snapshot;
@@ -19,6 +24,8 @@ export default {
19
24
  const violations = [];
20
25
 
21
26
  for (const [key, block] of Object.entries(snapshot)) {
27
+ checkStateRole(key, block, violations);
28
+
22
29
  for (const field of block.fields ?? []) {
23
30
  if (field.interp === undefined) {
24
31
  continue;
@@ -43,3 +50,39 @@ export default {
43
50
  return verdict(violations);
44
51
  },
45
52
  };
53
+
54
+ // позиции зафиксированы движком (`GameMap::dynamic_map_data_with_state`)
55
+ const STATE_INDEX_FLAT = 3;
56
+ const STATE_INDEX_LAYERED = 5;
57
+
58
+ function checkStateRole(key, block, violations) {
59
+ const fields = block.fields ?? [];
60
+ const layered = fields[3]?.role === 'z' && fields[4]?.role === 'level';
61
+ const expected = layered ? STATE_INDEX_LAYERED : STATE_INDEX_FLAT;
62
+
63
+ fields.forEach((field, index) => {
64
+ if (field.role !== 'state') {
65
+ return;
66
+ }
67
+
68
+ const at = `snapshot "${key}".${field.name}`;
69
+
70
+ if (index !== expected) {
71
+ violations.push(
72
+ `${at}: role 'state' at index ${index}, the engine writes it at ` +
73
+ `${expected} (right after the ${layered ? 'layered' : 'flat'} row head)`,
74
+ );
75
+ }
76
+
77
+ if (field.ty !== 'u8') {
78
+ violations.push(`${at}: role 'state' must be ty "u8", got "${field.ty}"`);
79
+ }
80
+
81
+ if (block.optionalFrom !== undefined && block.optionalFrom <= index) {
82
+ violations.push(
83
+ `${at}: role 'state' at index ${index} lies inside the optional tail ` +
84
+ `(optionalFrom ${block.optionalFrom}) — a resting body would lose it`,
85
+ );
86
+ }
87
+ });
88
+ }
@@ -0,0 +1,58 @@
1
+ import { ERROR, skip, verdict } from '../result.js';
2
+
3
+ // Поле карты `game` (и `physicsDynamic[i].game`) — пространство игры:
4
+ // движок его не читает и не масштабирует, но фиксирует форму. Ядро
5
+ // (`MapConfig::validate`) отвергает не-объект при загрузке карты, то есть в
6
+ // уже запущенном матче; здесь то же ловится до сборки.
7
+ export default {
8
+ id: 'E7',
9
+ name: 'mapGameField',
10
+ level: ERROR,
11
+ title: 'map game field is a plain object',
12
+
13
+ check(ctx) {
14
+ const maps = ctx.gameConfig?.maps;
15
+
16
+ if (!maps) {
17
+ return skip('no gameConfig.maps');
18
+ }
19
+
20
+ const declared = Object.entries(maps).filter(
21
+ ([, map]) =>
22
+ map?.game !== undefined ||
23
+ (map?.physicsDynamic ?? []).some(object => object?.game !== undefined),
24
+ );
25
+
26
+ if (!declared.length) {
27
+ return skip('no map declares game');
28
+ }
29
+
30
+ const violations = [];
31
+
32
+ for (const [name, map] of declared) {
33
+ if (map.game !== undefined && !isPlainObject(map.game)) {
34
+ violations.push(`map "${name}": game is not a plain object`);
35
+ }
36
+
37
+ for (const [index, object] of (map.physicsDynamic ?? []).entries()) {
38
+ if (object?.game !== undefined && !isPlainObject(object.game)) {
39
+ violations.push(
40
+ `map "${name}": physicsDynamic ${index} game is not a plain object`,
41
+ );
42
+ }
43
+ }
44
+ }
45
+
46
+ return verdict(violations);
47
+ },
48
+ };
49
+
50
+ // `null` ядро принимает как «не объявлено», но в исходнике карты это почти
51
+ // всегда опечатка — правило требует либо объект, либо отсутствие поля
52
+ function isPlainObject(value) {
53
+ return (
54
+ value !== null &&
55
+ typeof value === 'object' &&
56
+ Object.getPrototypeOf(value) === Object.prototype
57
+ );
58
+ }
@@ -35,6 +35,7 @@ import e3 from './e3-sound-registry.js';
35
35
  import e4 from './e4-map-layers.js';
36
36
  import e5 from './e5-map-radar-walls.js';
37
37
  import e6 from './e6-sound-spatial.js';
38
+ import e7 from './e7-map-game-field.js';
38
39
 
39
40
  // Порядок групп — порядок отчёта: A пакет и сборка, B host, C client,
40
41
  // D снапшот, E ассеты (plan/create-vimp-game/stage_1.md).
@@ -43,7 +44,7 @@ export const rules = [
43
44
  b1, b2, b3, b4, b5, b6, b7, b8, b9, b10,
44
45
  c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11,
45
46
  d1, d2, d3,
46
- e1, e2, e3, e4, e5, e6,
47
+ e1, e2, e3, e4, e5, e6, e7,
47
48
  ];
48
49
 
49
50
  export default rules;
@@ -35,6 +35,14 @@ export const ENGINE_CAPABILITIES = createRegistry('engine-capabilities', [
35
35
  // уклон рампы в ядре, поле `levelHeight` в контексте статического парта,
36
36
  // прогоны рамп уровня (`ramps`) там же, коллайдеры-стражи прогона
37
37
  { value: 'map.levelHeight', since: '0.32.0' },
38
+ // непрозрачное поле карты `game` (и `physicsDynamic[i].game`): до
39
+ // игрового ядра хоста, `ClientCore.set_map` и партов карты; хук ядра
40
+ // `GameSim::on_map_loaded`
41
+ { value: 'map.gameData', since: '0.34.0' },
42
+ // байт состояния тела карты (роль `state` в строке динамики,
43
+ // `SimCtx::map_body_state`) и доступ игры к телам карты по индексу
44
+ // (хэндл, индекс по хэндлу, отключение тела)
45
+ { value: 'map.bodyState', since: '0.34.0' },
38
46
  ]);
39
47
 
40
48
  // плоский список имён — то, чем правило контракта B2 проверяет `requires`