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 +1 -1
- package/package.json +1 -1
- package/src/client/main.js +5 -0
- package/src/devtools/VirtualClient.js +2 -0
- package/src/devtools/contract/rules/d3-snapshot-interp.js +44 -1
- package/src/devtools/contract/rules/e7-map-game-field.js +58 -0
- package/src/devtools/contract/rules/index.js +2 -1
- package/src/lib/capabilities.js +8 -0
package/core/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "vimp-engine-core"
|
|
3
|
-
version = "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
package/src/client/main.js
CHANGED
|
@@ -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;
|
|
@@ -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;
|
package/src/lib/capabilities.js
CHANGED
|
@@ -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`
|