vimp-engine 0.8.0 → 0.10.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/bin/vimp-contract.js +110 -0
- package/core/Cargo.toml +17 -0
- package/package.json +4 -2
- package/src/client/components/view/Game.js +8 -12
- package/src/client/main.js +5 -0
- package/src/devtools/contract/format.js +65 -0
- package/src/devtools/contract/index.js +69 -0
- package/src/devtools/contract/loadContext.js +250 -0
- package/src/devtools/contract/result.js +55 -0
- package/src/devtools/contract/rules/a1-package-fields.js +81 -0
- package/src/devtools/contract/rules/a2-package-scripts.js +36 -0
- package/src/devtools/contract/rules/a3-entry-paths.js +30 -0
- package/src/devtools/contract/rules/a4-vite-config.js +47 -0
- package/src/devtools/contract/rules/a5-cargo-core.js +194 -0
- package/src/devtools/contract/rules/a6-manifest.js +78 -0
- package/src/devtools/contract/rules/b1-host-shape.js +50 -0
- package/src/devtools/contract/rules/b10-respawns.js +52 -0
- package/src/devtools/contract/rules/b2-engine-api.js +47 -0
- package/src/devtools/contract/rules/b3-game-config-shape.js +27 -0
- package/src/devtools/contract/rules/b4-teams.js +37 -0
- package/src/devtools/contract/rules/b5-room-form.js +43 -0
- package/src/devtools/contract/rules/b6-panel-reserved-key.js +29 -0
- package/src/devtools/contract/rules/b7-chat-commands.js +49 -0
- package/src/devtools/contract/rules/b8-system-messages.js +48 -0
- package/src/devtools/contract/rules/b9-vote-names.js +48 -0
- package/src/devtools/contract/rules/c1-client-shape.js +52 -0
- package/src/devtools/contract/rules/c10-auth-schema.js +55 -0
- package/src/devtools/contract/rules/c2-parts-registered.js +43 -0
- package/src/devtools/contract/rules/c3-game-sets-coverage.js +35 -0
- package/src/devtools/contract/rules/c4-component-dependencies.js +33 -0
- package/src/devtools/contract/rules/c5-panel-time-field.js +42 -0
- package/src/devtools/contract/rules/c6-stat-columns.js +36 -0
- package/src/devtools/contract/rules/c7-key-sets.js +71 -0
- package/src/devtools/contract/rules/c8-baked-assets.js +34 -0
- package/src/devtools/contract/rules/c9-chat-messages.js +41 -0
- package/src/devtools/contract/rules/d1-snapshot-ids.js +36 -0
- package/src/devtools/contract/rules/d2-snapshot-classes.js +42 -0
- package/src/devtools/contract/rules/d3-snapshot-interp.js +45 -0
- package/src/devtools/contract/rules/e1-sound-pairs.js +40 -0
- package/src/devtools/contract/rules/e2-map-images.js +40 -0
- package/src/devtools/contract/rules/e3-sound-registry.js +25 -0
- package/src/devtools/contract/rules/index.js +44 -0
- package/src/host/meta/player/ParticipantManager.js +6 -0
- package/tests/fixtures/miniGame/config/client.js +10 -2
- package/tests/fixtures/miniGame/config/game.js +9 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ERROR, skip, verdict } from '../result.js';
|
|
2
|
+
|
|
3
|
+
// Уникальность id блоков — единственное, что вообще валидируется в
|
|
4
|
+
// снапшот-схеме. Дубликат означает, что один блок кадра разбирается
|
|
5
|
+
// схемой другого: мусор вместо ошибки.
|
|
6
|
+
export default {
|
|
7
|
+
id: 'D1',
|
|
8
|
+
name: 'snapshotIds',
|
|
9
|
+
level: ERROR,
|
|
10
|
+
title: 'snapshot block ids are unique',
|
|
11
|
+
|
|
12
|
+
check(ctx) {
|
|
13
|
+
const snapshot = ctx.gameConfig?.snapshot;
|
|
14
|
+
|
|
15
|
+
if (!snapshot) {
|
|
16
|
+
return skip('no gameConfig.snapshot');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const byId = new Map();
|
|
20
|
+
const violations = [];
|
|
21
|
+
|
|
22
|
+
for (const [key, block] of Object.entries(snapshot)) {
|
|
23
|
+
const previous = byId.get(block.id);
|
|
24
|
+
|
|
25
|
+
if (previous !== undefined) {
|
|
26
|
+
violations.push(
|
|
27
|
+
`snapshot id ${block.id} is used by both "${previous}" and "${key}"`,
|
|
28
|
+
);
|
|
29
|
+
} else {
|
|
30
|
+
byId.set(block.id, key);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return verdict(violations);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ERROR, skip, verdict } from '../result.js';
|
|
2
|
+
|
|
3
|
+
// Горячий буфер несёт только indexed8/indexedNoNull8; list16 и indexed32
|
|
4
|
+
// приходят через take_frames() и обязаны быть class: 'event' — иначе кадр
|
|
5
|
+
// не считается надёжным и может быть выброшен ненадёжным каналом.
|
|
6
|
+
const HOT_KINDS = ['indexed8', 'indexedNoNull8'];
|
|
7
|
+
const EVENT_KINDS = ['list16', 'indexed32'];
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
id: 'D2',
|
|
11
|
+
name: 'snapshotClasses',
|
|
12
|
+
level: ERROR,
|
|
13
|
+
title: "class 'hot' only on indexed8/indexedNoNull8 blocks",
|
|
14
|
+
|
|
15
|
+
check(ctx) {
|
|
16
|
+
const snapshot = ctx.gameConfig?.snapshot;
|
|
17
|
+
|
|
18
|
+
if (!snapshot) {
|
|
19
|
+
return skip('no gameConfig.snapshot');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const violations = [];
|
|
23
|
+
|
|
24
|
+
for (const [key, block] of Object.entries(snapshot)) {
|
|
25
|
+
if (block.class === 'hot' && !HOT_KINDS.includes(block.kind)) {
|
|
26
|
+
violations.push(
|
|
27
|
+
`snapshot "${key}": kind "${block.kind}" cannot be class 'hot' ` +
|
|
28
|
+
`(hot buffer carries ${HOT_KINDS.join(' / ')})`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (EVENT_KINDS.includes(block.kind) && block.class !== 'event') {
|
|
33
|
+
violations.push(
|
|
34
|
+
`snapshot "${key}": kind "${block.kind}" must be class 'event', ` +
|
|
35
|
+
`got "${block.class}" — the frame is not classified as reliable`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return verdict(violations);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ERROR, skip, verdict } from '../result.js';
|
|
2
|
+
|
|
3
|
+
// Интерполируются только f32 и только в горячих блоках. interp на u8 или
|
|
4
|
+
// в event-блоке не ошибка сборки — он просто не исполняется, и поле
|
|
5
|
+
// дёргается вместо плавного движения.
|
|
6
|
+
export default {
|
|
7
|
+
id: 'D3',
|
|
8
|
+
name: 'snapshotInterp',
|
|
9
|
+
level: ERROR,
|
|
10
|
+
title: "interp only on f32 fields of class 'hot' blocks",
|
|
11
|
+
|
|
12
|
+
check(ctx) {
|
|
13
|
+
const snapshot = ctx.gameConfig?.snapshot;
|
|
14
|
+
|
|
15
|
+
if (!snapshot) {
|
|
16
|
+
return skip('no gameConfig.snapshot');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const violations = [];
|
|
20
|
+
|
|
21
|
+
for (const [key, block] of Object.entries(snapshot)) {
|
|
22
|
+
for (const field of block.fields ?? []) {
|
|
23
|
+
if (field.interp === undefined) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (block.class !== 'hot') {
|
|
28
|
+
violations.push(
|
|
29
|
+
`snapshot "${key}".${field.name}: interp on a class ` +
|
|
30
|
+
`"${block.class}" block is never applied`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (field.ty !== 'f32') {
|
|
35
|
+
violations.push(
|
|
36
|
+
`snapshot "${key}".${field.name}: interp on ty "${field.ty}" — ` +
|
|
37
|
+
'only f32 interpolates',
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return verdict(violations);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ERROR, skip, verdict } from '../result.js';
|
|
2
|
+
|
|
3
|
+
// Клиент перебирает codecList ['webm', 'mp3'] и берёт первый поддержанный
|
|
4
|
+
// браузером. Отсутствующий .mp3 не ломает Chrome и ломает Safari — то есть
|
|
5
|
+
// проявляется у части игроков и никогда у автора.
|
|
6
|
+
export default {
|
|
7
|
+
id: 'E1',
|
|
8
|
+
name: 'soundPairs',
|
|
9
|
+
level: ERROR,
|
|
10
|
+
title: 'every declared sound ships as a webm + mp3 pair',
|
|
11
|
+
|
|
12
|
+
check(ctx) {
|
|
13
|
+
const sounds = ctx.clientConfig?.parts?.sounds?.sounds;
|
|
14
|
+
|
|
15
|
+
if (!ctx.distFiles) {
|
|
16
|
+
return skip('not built — no dist/');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!sounds) {
|
|
20
|
+
return skip('no sound registry');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const violations = [];
|
|
24
|
+
|
|
25
|
+
for (const [name, sound] of Object.entries(sounds)) {
|
|
26
|
+
const file = sound?.file ?? name;
|
|
27
|
+
|
|
28
|
+
for (const codec of ctx.clientConfig.parts.sounds.codecList ?? [
|
|
29
|
+
'webm',
|
|
30
|
+
'mp3',
|
|
31
|
+
]) {
|
|
32
|
+
if (!ctx.distFiles.has(`sounds/${file}.${codec}`)) {
|
|
33
|
+
violations.push(`sound "${name}": dist/sounds/${file}.${codec} is missing`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return verdict(violations);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ERROR, skip, verdict } from '../result.js';
|
|
2
|
+
|
|
3
|
+
// Тайл-лист и картинки динамических тел везёт пакет игры (dist/img/),
|
|
4
|
+
// движок игровых картинок не раздаёт. Карта, назвавшая отсутствующий
|
|
5
|
+
// файл, падает молча в рантайме: полотно пустое, консоль чистая.
|
|
6
|
+
export default {
|
|
7
|
+
id: 'E2',
|
|
8
|
+
name: 'mapImages',
|
|
9
|
+
level: ERROR,
|
|
10
|
+
title: 'map images exist in dist/img/',
|
|
11
|
+
|
|
12
|
+
check(ctx) {
|
|
13
|
+
const maps = ctx.gameConfig?.maps;
|
|
14
|
+
|
|
15
|
+
if (!ctx.distFiles) {
|
|
16
|
+
return skip('not built — no dist/');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!maps) {
|
|
20
|
+
return skip('no gameConfig.maps');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const violations = [];
|
|
24
|
+
|
|
25
|
+
for (const [name, map] of Object.entries(maps)) {
|
|
26
|
+
const images = [
|
|
27
|
+
map?.spriteSheet?.img,
|
|
28
|
+
...(map?.physicsDynamic ?? []).map(body => body?.img),
|
|
29
|
+
].filter(Boolean);
|
|
30
|
+
|
|
31
|
+
for (const img of new Set(images)) {
|
|
32
|
+
if (!ctx.distFiles.has(`img/${img}`)) {
|
|
33
|
+
violations.push(`map "${name}": dist/img/${img} is missing`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return verdict(violations);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { WARN, skip, verdict } from '../result.js';
|
|
2
|
+
|
|
3
|
+
// Игра без единого звука собирается и играется — это законная стадия
|
|
4
|
+
// разработки, а не отказ. Но пустой реестр почти всегда означает
|
|
5
|
+
// «забыли», поэтому warn: под --strict он становится ошибкой.
|
|
6
|
+
export default {
|
|
7
|
+
id: 'E3',
|
|
8
|
+
name: 'soundRegistry',
|
|
9
|
+
level: WARN,
|
|
10
|
+
title: 'the sound registry is not empty',
|
|
11
|
+
|
|
12
|
+
check(ctx) {
|
|
13
|
+
if (!ctx.distFiles) {
|
|
14
|
+
return skip('not built — no dist/');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const sounds = ctx.clientConfig?.parts?.sounds?.sounds;
|
|
18
|
+
|
|
19
|
+
if (sounds && Object.keys(sounds).length) {
|
|
20
|
+
return verdict([]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return verdict(['no sounds are registered in client parts.sounds']);
|
|
24
|
+
},
|
|
25
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import a1 from './a1-package-fields.js';
|
|
2
|
+
import a2 from './a2-package-scripts.js';
|
|
3
|
+
import a3 from './a3-entry-paths.js';
|
|
4
|
+
import a4 from './a4-vite-config.js';
|
|
5
|
+
import a5 from './a5-cargo-core.js';
|
|
6
|
+
import a6 from './a6-manifest.js';
|
|
7
|
+
import b1 from './b1-host-shape.js';
|
|
8
|
+
import b2 from './b2-engine-api.js';
|
|
9
|
+
import b3 from './b3-game-config-shape.js';
|
|
10
|
+
import b4 from './b4-teams.js';
|
|
11
|
+
import b5 from './b5-room-form.js';
|
|
12
|
+
import b6 from './b6-panel-reserved-key.js';
|
|
13
|
+
import b7 from './b7-chat-commands.js';
|
|
14
|
+
import b8 from './b8-system-messages.js';
|
|
15
|
+
import b9 from './b9-vote-names.js';
|
|
16
|
+
import b10 from './b10-respawns.js';
|
|
17
|
+
import c1 from './c1-client-shape.js';
|
|
18
|
+
import c2 from './c2-parts-registered.js';
|
|
19
|
+
import c3 from './c3-game-sets-coverage.js';
|
|
20
|
+
import c4 from './c4-component-dependencies.js';
|
|
21
|
+
import c5 from './c5-panel-time-field.js';
|
|
22
|
+
import c6 from './c6-stat-columns.js';
|
|
23
|
+
import c7 from './c7-key-sets.js';
|
|
24
|
+
import c8 from './c8-baked-assets.js';
|
|
25
|
+
import c9 from './c9-chat-messages.js';
|
|
26
|
+
import c10 from './c10-auth-schema.js';
|
|
27
|
+
import d1 from './d1-snapshot-ids.js';
|
|
28
|
+
import d2 from './d2-snapshot-classes.js';
|
|
29
|
+
import d3 from './d3-snapshot-interp.js';
|
|
30
|
+
import e1 from './e1-sound-pairs.js';
|
|
31
|
+
import e2 from './e2-map-images.js';
|
|
32
|
+
import e3 from './e3-sound-registry.js';
|
|
33
|
+
|
|
34
|
+
// Порядок групп — порядок отчёта: A пакет и сборка, B host, C client,
|
|
35
|
+
// D снапшот, E ассеты (plan/create-vimp-game/stage_1.md).
|
|
36
|
+
export const rules = [
|
|
37
|
+
a1, a2, a3, a4, a5, a6,
|
|
38
|
+
b1, b2, b3, b4, b5, b6, b7, b8, b9, b10,
|
|
39
|
+
c1, c2, c3, c4, c5, c6, c7, c8, c9, c10,
|
|
40
|
+
d1, d2, d3,
|
|
41
|
+
e1, e2, e3,
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export default rules;
|
|
@@ -242,6 +242,12 @@ class ParticipantManager {
|
|
|
242
242
|
get isFull() {
|
|
243
243
|
return this._maxPlayers ? this.totalCount >= this._maxPlayers : false;
|
|
244
244
|
}
|
|
245
|
+
|
|
246
|
+
// потолок комнаты: игре он нужен, чтобы ограничивать пользовательский
|
|
247
|
+
// ввод до цикла (`/spawn 1e9`), а не упираться в isFull на каждой итерации
|
|
248
|
+
get maxPlayers() {
|
|
249
|
+
return this._maxPlayers;
|
|
250
|
+
}
|
|
245
251
|
}
|
|
246
252
|
|
|
247
253
|
export default ParticipantManager;
|
|
@@ -8,6 +8,10 @@ export default {
|
|
|
8
8
|
// parts; setId карты (m1) — по нему создаётся статика полотна
|
|
9
9
|
a1: ['Actor'],
|
|
10
10
|
m1: ['Actor'],
|
|
11
|
+
// event-ключ схемы: строк он в фикстуре не даёт, но запись нужна —
|
|
12
|
+
// ключ снапшота без набора партов это чёрный холст на первом же
|
|
13
|
+
// кадре, где он ожил
|
|
14
|
+
e1: ['Actor'],
|
|
11
15
|
},
|
|
12
16
|
entitiesOnCanvas: {
|
|
13
17
|
Actor: 'vimp',
|
|
@@ -35,8 +39,12 @@ export default {
|
|
|
35
39
|
|
|
36
40
|
controls: {
|
|
37
41
|
keySetList: [
|
|
38
|
-
// spectator keyset
|
|
39
|
-
|
|
42
|
+
// spectator keyset: переключение камеры — механизм движка
|
|
43
|
+
// (hostDefaults.spectatorKeys), без него наблюдатель заперт
|
|
44
|
+
{
|
|
45
|
+
78: 'nextPlayer', // next player (n)
|
|
46
|
+
80: 'prevPlayer', // prev player (p)
|
|
47
|
+
},
|
|
40
48
|
// player keyset
|
|
41
49
|
{
|
|
42
50
|
87: 'forward', // forward (w)
|
|
@@ -45,15 +45,23 @@ export default {
|
|
|
45
45
|
arena: {
|
|
46
46
|
setId: 'm1',
|
|
47
47
|
scale: 1,
|
|
48
|
-
|
|
48
|
+
// имя файла — инертные данные: фикстура host-only, рендера в ней нет
|
|
49
|
+
// и картинку никто не грузит. Реальные тайлы везёт пакет игры
|
|
50
|
+
// (`${assetsBase}img/`), движок картинок не раздаёт
|
|
51
|
+
spriteSheet: { img: 'fixture-tiles.png', frames: [[0, 0, 32, 32]] },
|
|
49
52
|
layers: { 1: [0] },
|
|
50
53
|
physicsStatic: [1],
|
|
51
54
|
physicsDynamic: [],
|
|
52
55
|
step: 32,
|
|
56
|
+
// точек не меньше, чем roomDefaults.maxPlayers: длина списка —
|
|
57
|
+
// жёсткая вместимость команды на карте (RoundManager), и комната
|
|
58
|
+
// молча вмещала бы меньше, чем обещает лобби
|
|
53
59
|
respawns: {
|
|
54
60
|
team1: [
|
|
55
61
|
[100, 100, 0],
|
|
56
62
|
[200, 100, 0],
|
|
63
|
+
[100, 200, 0],
|
|
64
|
+
[200, 200, 0],
|
|
57
65
|
],
|
|
58
66
|
},
|
|
59
67
|
map: [
|