vimp-engine 0.39.0 → 0.40.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/README.md +2 -2
- package/bin/vimp-sim.js +2 -2
- package/core/Cargo.toml +2 -2
- package/package.json +3 -4
- package/src/client/boot.js +11 -87
- package/src/client/components/model/RoomMenu.js +1 -1
- package/src/client/components/view/Auth.js +3 -3
- package/src/client/lib/formBuilder.js +7 -6
- package/src/client/lib/hostVoteCommand.js +2 -2
- package/src/client/lib/pickActiveGame.js +3 -12
- package/src/client/lib/retryOnNetworkError.js +0 -9
- package/src/client/main.js +43 -90
- package/src/client/network/SessionSupervisor.js +14 -14
- package/src/client/network/WebRtcManager.js +1 -2
- package/src/client/network/policyClose.js +10 -77
- package/src/client/style.css +4 -6
- package/src/client/views/gameShell.js +2 -4
- package/src/client/views/includes/auth.pug +1 -4
- package/src/client/views/includes/lobby.pug +3 -6
- package/src/config/closeCodes.js +7 -9
- package/src/config/gameCodes.js +1 -1
- package/src/config/hostDefaults.js +2 -2
- package/src/config/lobby.js +3 -4
- package/src/config/wsports.js +1 -1
- package/src/devtools/ScenarioRunner.js +1 -1
- package/src/devtools/builtinScenario.js +2 -2
- package/src/devtools/contract/rules/a2-package-scripts.js +1 -1
- package/src/devtools/contract/rules/a4-vite-config.js +1 -1
- package/src/devtools/contract/rules/c4-component-dependencies.js +1 -1
- package/src/devtools/invariants.js +1 -1
- package/src/devtools/pluginLoader.js +2 -2
- package/src/host/HostGame.js +4 -4
- package/src/host/PortMachine.js +4 -21
- package/src/host/commandFailure.js +2 -2
- package/src/host/identity.js +3 -3
- package/src/host/meta/core/CommandProcessor.js +2 -2
- package/src/host/meta/modules/Accolades.js +1 -1
- package/src/lib/createHostRuntime.js +1 -1
- package/src/lib/loadGamePackage.js +7 -9
- package/src/lib/offlinePlayerData.js +1 -1
- package/src/lib/validators.js +2 -2
- package/src/lib/voicePacket.js +1 -1
- package/src/standalone/index.js +3 -2
- package/src/client/lib/footerLink.js +0 -30
- package/src/client/network/WebSocketTransport.js +0 -78
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ The VIMP engine SDK for game plugins: the browser host and client runtime,
|
|
|
4
4
|
the standalone mode (`vimp-engine/standalone`) and the CLI tools `vimp-sim`
|
|
5
5
|
and `vimp-contract`. Game rules live in separate plugin packages that the
|
|
6
6
|
engine loads at runtime through the
|
|
7
|
-
|
|
7
|
+
plugin contract (VIMP docs: plugin-api.md).
|
|
8
8
|
|
|
9
9
|
Start a game:
|
|
10
10
|
|
|
@@ -12,4 +12,4 @@ Start a game:
|
|
|
12
12
|
npm create vimp-game my-game
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Platform: https://vimp.lgick.dev
|
package/bin/vimp-sim.js
CHANGED
|
@@ -40,8 +40,8 @@ const BUILTIN_NOTICE =
|
|
|
40
40
|
'notice: the built-in smoke scenario is running against --game. It only\n' +
|
|
41
41
|
'notice: proves the loop closes on this plugin: key coverage (invariant 2)\n' +
|
|
42
42
|
'notice: and prediction drift (invariant 9) are skipped — both need a\n' +
|
|
43
|
-
'notice: scenario written for your game
|
|
44
|
-
'notice:
|
|
43
|
+
'notice: scenario written for your game; the format is in\n' +
|
|
44
|
+
'notice: VIMP docs: debugging.md § "Scenario format". Then run\n' +
|
|
45
45
|
'notice: vimp-sim --scenario <file>.\n\n';
|
|
46
46
|
|
|
47
47
|
// типичная опечатка «--core без --game»: фикстурное ядро — обычный JS и
|
package/core/Cargo.toml
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "vimp-engine-core"
|
|
3
|
-
version = "0.23.
|
|
3
|
+
version = "0.23.5"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
description = "VIMP engine simulation core: physics, map, snapshot framing, interpolation/prediction/raycast primitives, navigation utilities. No wasm-bindgen — the game crate builds the WASM ABI wrappers."
|
|
6
6
|
license = "MIT"
|
|
7
|
-
homepage = "https://
|
|
7
|
+
homepage = "https://vimp.lgick.dev"
|
|
8
8
|
documentation = "https://docs.rs/vimp-engine-core"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vimp-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "VIMP engine SDK: browser host and client runtime, standalone mode and CLI tools (vimp-sim, vimp-contract) for VIMP game plugins",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"url": "git+ssh://git@github.com/lgick/vimp.git",
|
|
9
9
|
"directory": "packages/engine"
|
|
10
10
|
},
|
|
11
|
-
"homepage": "https://
|
|
11
|
+
"homepage": "https://vimp.lgick.dev",
|
|
12
12
|
"type": "module",
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=20.11.0"
|
|
@@ -44,8 +44,7 @@
|
|
|
44
44
|
],
|
|
45
45
|
"scripts": {
|
|
46
46
|
"predev": "node scripts/sync-pixi-vendor.mjs",
|
|
47
|
-
"dev": "NODE_ENV=development nodemon src/master/main.js -w src/master -w src/
|
|
48
|
-
"dedicated": "NODE_ENV=development nodemon src/master/main.js -w src/master -w src/dedicated -w src/host -w src/lib -w src/config",
|
|
47
|
+
"dev": "NODE_ENV=development nodemon src/master/main.js -w src/master -w src/lib -w src/config",
|
|
49
48
|
"start": "NODE_ENV=production node --env-file ../../.env src/master/main.js",
|
|
50
49
|
"prebuild": "node scripts/sync-pixi-vendor.mjs",
|
|
51
50
|
"build": "vite build",
|
package/src/client/boot.js
CHANGED
|
@@ -1,33 +1,19 @@
|
|
|
1
1
|
// Режим загрузки клиента движка (Этап 2 плана standalone-sdk). Клиент один
|
|
2
|
-
// на
|
|
2
|
+
// на оба контура, а различает их этот модуль:
|
|
3
3
|
//
|
|
4
|
-
// lobby
|
|
5
|
-
// solo
|
|
6
|
-
//
|
|
7
|
-
// dedicated — Node-сервер: прямой WebSocket к игровому процессу.
|
|
4
|
+
// lobby — прод: каталог игр и сигналинг мастера, лобби за экраном профиля;
|
|
5
|
+
// solo — standalone SDK: хост в этой же вкладке (inline, без Worker'а),
|
|
6
|
+
// мастера нет вовсе.
|
|
8
7
|
//
|
|
9
8
|
// Канал передачи конфига от SDK к main.js — модульное состояние: и SDK, и
|
|
10
9
|
// main.js резолвят этот модуль в один экземпляр графа бандлера. Глобалов на
|
|
11
10
|
// window не вводим: они переживают перезагрузку бандла и ломают вторую
|
|
12
11
|
// вкладку SDK в том же документе.
|
|
13
12
|
|
|
14
|
-
import {
|
|
15
|
-
bodyReadFailure,
|
|
16
|
-
fetchRetrying,
|
|
17
|
-
isNetworkFailure,
|
|
18
|
-
} from './lib/retryOnNetworkError.js';
|
|
19
|
-
|
|
20
|
-
// эндпоинт режима: его отдаёт dedicated-сервер, у мастера его нет —
|
|
21
|
-
// 404 и мусор означают прод-лобби; сбой сети — отказ загрузки
|
|
22
|
-
const CONFIG_URL = '/config';
|
|
23
|
-
|
|
24
|
-
// умолчание игрового WS dedicated-сервера: тот же хост, путь /game
|
|
25
|
-
const DEDICATED_WS_PATH = '/game';
|
|
26
|
-
|
|
27
13
|
/**
|
|
28
14
|
* Конфиг загрузки:
|
|
29
15
|
* {
|
|
30
|
-
* mode: 'lobby' | 'solo'
|
|
16
|
+
* mode: 'lobby' | 'solo',
|
|
31
17
|
* container, // solo: точка монтирования каркаса и канвасов
|
|
32
18
|
* manifest, // solo: манифест-подобный объект в памяти
|
|
33
19
|
* clientPlugin, // solo: живой ClientPlugin (минуя dynamic import)
|
|
@@ -37,15 +23,13 @@ const DEDICATED_WS_PATH = '/game';
|
|
|
37
23
|
* startupVotes, // solo: ответы на initialVote (выход из наблюдателей)
|
|
38
24
|
* startupCommands, // solo: чат-команды игры (спавн ботов)
|
|
39
25
|
* reportUrl, // solo: приём журнала клиентских ошибок (null — выкл.)
|
|
40
|
-
* wsUrl, // dedicated: адрес игрового WS
|
|
41
|
-
* gameId, // dedicated: какую игру каталога считать активной
|
|
42
26
|
* }
|
|
43
27
|
*/
|
|
44
28
|
let bootConfig = null;
|
|
45
29
|
|
|
46
30
|
/**
|
|
47
31
|
* Задаёт конфиг загрузки. SDK вызывает его до import('./main.js') — main.js
|
|
48
|
-
* читает уже готовое
|
|
32
|
+
* читает уже готовое состояние.
|
|
49
33
|
* @param {Object} cfg - Конфиг загрузки (см. выше).
|
|
50
34
|
*/
|
|
51
35
|
export function setBootConfig(cfg) {
|
|
@@ -53,7 +37,7 @@ export function setBootConfig(cfg) {
|
|
|
53
37
|
}
|
|
54
38
|
|
|
55
39
|
/**
|
|
56
|
-
* Текущий конфиг загрузки (null, пока
|
|
40
|
+
* Текущий конфиг загрузки (null, пока SDK его не задал).
|
|
57
41
|
* @returns {Object|null}
|
|
58
42
|
*/
|
|
59
43
|
export function getBootConfig() {
|
|
@@ -69,69 +53,9 @@ export function resetBootConfig() {
|
|
|
69
53
|
}
|
|
70
54
|
|
|
71
55
|
/**
|
|
72
|
-
* Разрешает режим загрузки: инъекция SDK
|
|
73
|
-
* @
|
|
74
|
-
* @param {Object} [retry] - Параметры fetchRetrying (тесты отключают паузы).
|
|
75
|
-
* @returns {Promise<Object>} Конфиг загрузки; отклоняется NetworkFailure, если
|
|
76
|
-
* сеть не ответила после повторов.
|
|
56
|
+
* Разрешает режим загрузки: инъекция SDK, иначе лобби.
|
|
57
|
+
* @returns {Object} Конфиг загрузки.
|
|
77
58
|
*/
|
|
78
|
-
export
|
|
79
|
-
|
|
80
|
-
return bootConfig;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const doFetch = fetchImpl ?? ((...args) => fetch(...args));
|
|
84
|
-
let res = null;
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
res = await fetchRetrying(doFetch, CONFIG_URL, retry);
|
|
88
|
-
} catch (e) {
|
|
89
|
-
// сети нет: режим неизвестен, и лобби здесь — догадка, а не факт
|
|
90
|
-
if (isNetworkFailure(e)) {
|
|
91
|
-
throw e;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (res?.ok) {
|
|
96
|
-
try {
|
|
97
|
-
bootConfig = normalize(await res.json());
|
|
98
|
-
} catch (e) {
|
|
99
|
-
// мусор в JSON — лобби; обрыв при чтении тела — сбой сети
|
|
100
|
-
const failure = bodyReadFailure(e);
|
|
101
|
-
|
|
102
|
-
if (isNetworkFailure(failure)) {
|
|
103
|
-
throw failure;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
bootConfig = null;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
bootConfig = bootConfig ?? { mode: 'lobby' };
|
|
111
|
-
|
|
112
|
-
return bootConfig;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// приводит ответ /config к валидному конфигу; всё, что не dedicated, —
|
|
116
|
-
// лобби: неизвестный режим не должен уронить клиент в чёрный экран
|
|
117
|
-
function normalize(raw) {
|
|
118
|
-
if (!raw || typeof raw !== 'object' || raw.mode !== 'dedicated') {
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
...raw,
|
|
124
|
-
mode: 'dedicated',
|
|
125
|
-
// сервер объявляет свой путь в /config: константа — только умолчание
|
|
126
|
-
wsUrl: raw.wsUrl || defaultDedicatedWsUrl(raw.wsPath),
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function defaultDedicatedWsUrl(wsPath) {
|
|
131
|
-
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
132
|
-
// пустой или отсутствующий путь — это «умолчание», а не корень: `''` дал бы
|
|
133
|
-
// ws://host, null — ws://hostnull
|
|
134
|
-
const path = wsPath || DEDICATED_WS_PATH;
|
|
135
|
-
|
|
136
|
-
return `${protocol}//${location.host}${path}`;
|
|
59
|
+
export function resolveBootConfig() {
|
|
60
|
+
return bootConfig ?? { mode: 'lobby' };
|
|
137
61
|
}
|
|
@@ -8,7 +8,7 @@ import Publisher from '../../../lib/Publisher.js';
|
|
|
8
8
|
// - «Hand over host» — только у текущего хоста, когда в комнате есть другие
|
|
9
9
|
// люди и мастер назначил бету (иначе передавать некому).
|
|
10
10
|
// Пока идёт передача, пункты заблокированы, на их месте статус. Только
|
|
11
|
-
// лобби-режим: в solo
|
|
11
|
+
// лобби-режим: в solo ссылки на комнату нет
|
|
12
12
|
|
|
13
13
|
// handoff: null | 'pending' | 'slow' | 'failed'
|
|
14
14
|
const STATUS_TEXT = {
|
|
@@ -13,7 +13,7 @@ export default class AuthView {
|
|
|
13
13
|
// texts — игровые тексты формы (authSchema.texts: title, sections);
|
|
14
14
|
// сам каркас (auth.pug) нейтрален и текстов игры не содержит.
|
|
15
15
|
// params — та же дескрипторная схема формы, что и у room-формы
|
|
16
|
-
// (docs
|
|
16
|
+
// (VIMP docs: plugin-api.md § "Form schema"), едет по сети в
|
|
17
17
|
// PS_AUTH_DATA
|
|
18
18
|
constructor(model, elems, texts = null, params = null) {
|
|
19
19
|
if (authView) {
|
|
@@ -64,8 +64,8 @@ export default class AuthView {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// строит контролы формы игрока из той же дескрипторной схемы, что и
|
|
67
|
-
// room-форма (единый formBuilder,
|
|
68
|
-
// schema")
|
|
67
|
+
// room-форма (единый formBuilder,
|
|
68
|
+
// VIMP docs: plugin-api.md § "Form schema")
|
|
69
69
|
_renderFields(elems, params) {
|
|
70
70
|
if (!Array.isArray(params)) {
|
|
71
71
|
return;
|
|
@@ -4,9 +4,10 @@ import { resolveDescriptor } from '../../lib/formControls.js';
|
|
|
4
4
|
import { toDisplay, toStored, isNumericField } from '../../lib/formUnit.js';
|
|
5
5
|
|
|
6
6
|
// Общий билдер полей форм (room-форма и auth-форма используют один
|
|
7
|
-
// контракт дескрипторов —
|
|
8
|
-
// schema"). control:
|
|
9
|
-
// нативные элементы формы,
|
|
7
|
+
// контракт дескрипторов —
|
|
8
|
+
// VIMP docs: plugin-api.md § "Form schema"). control:
|
|
9
|
+
// 'select'|'text'|'checkbox'|'radio'; все контролы — нативные элементы формы,
|
|
10
|
+
// без визуальной кастомизации.
|
|
10
11
|
//
|
|
11
12
|
// Имя контрола приезжает от игры и разрешается через append-only реестр
|
|
12
13
|
// (lib/formControls.js): имена, выведенные из эксплуатации в v3 ('range',
|
|
@@ -270,8 +271,8 @@ const builders = {
|
|
|
270
271
|
// control» на имени, которое реестр считает живым
|
|
271
272
|
export const FORM_CONTROLS = Object.keys(builders);
|
|
272
273
|
|
|
273
|
-
// строит одно поле формы по дескриптору
|
|
274
|
-
// "Form schema")
|
|
274
|
+
// строит одно поле формы по дескриптору
|
|
275
|
+
// (VIMP docs: plugin-api.md § "Form schema")
|
|
275
276
|
export function buildField(descriptor, ctx = {}) {
|
|
276
277
|
// алиас выведенного контрола ('range' → numeric 'text') разрешается
|
|
277
278
|
// здесь же, где строится поле, и теми же правилами, что в валидации
|
|
@@ -292,7 +293,7 @@ export function buildField(descriptor, ctx = {}) {
|
|
|
292
293
|
}
|
|
293
294
|
|
|
294
295
|
// засеивает default каждого дескриптора room-формы значением из roomDefaults
|
|
295
|
-
// (docs
|
|
296
|
+
// (VIMP docs: plugin-api.md § "Form schema": roomDefaults остаётся
|
|
296
297
|
// единственным источником значений по умолчанию); явный descriptor.default,
|
|
297
298
|
// если он есть в схеме, побеждает. Вынесено из populateRoomForm (main.js),
|
|
298
299
|
// чтобы этот же код был накрыт тестом, а не продублирован в нём
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Голосование «Change host» (host-migration этап 10): команда /changehost и
|
|
2
2
|
// окно у гостей. Голоса считает мастер, поэтому команда в лобби-режиме не
|
|
3
|
-
// уходит хосту. В solo
|
|
3
|
+
// уходит хосту. В solo мастера нет — команда остаётся обычным
|
|
4
4
|
// текстом для игры.
|
|
5
5
|
|
|
6
6
|
// имя голосования движка; префикс '@' зарезервирован движком
|
|
@@ -13,7 +13,7 @@ const COMMAND = '/changehost';
|
|
|
13
13
|
/**
|
|
14
14
|
* Разбор сообщения чата.
|
|
15
15
|
* @param {string} text
|
|
16
|
-
* @param {string} bootMode - 'lobby' | 'solo'
|
|
16
|
+
* @param {string} bootMode - 'lobby' | 'solo'.
|
|
17
17
|
* @returns {'start'|'usage'|null} null — не перехватывать.
|
|
18
18
|
*/
|
|
19
19
|
export function parseChangeHost(text, bootMode) {
|
|
@@ -19,23 +19,14 @@ export function isGameAvailable(manifest) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* Активная игра
|
|
23
|
-
*
|
|
24
|
-
* Обе ветки фильтруются одинаково: сохранённый выбор игры (`gameId`) тоже мог
|
|
25
|
-
* указывать на игру, ставшую недоступной после обновления движка, и брать её
|
|
26
|
-
* «раз уж попросили» значило бы падать там, где можно объяснить.
|
|
22
|
+
* Активная игра вкладки: первая доступная игра каталога.
|
|
27
23
|
* @param {Array<Object>} manifests - Каталог мастера.
|
|
28
|
-
* @param {string} [gameId] - Сохранённый выбор игры (boot.gameId).
|
|
29
24
|
* @returns {Object|undefined} Манифест активной игры.
|
|
30
25
|
* @throws {Error} Если каталог непустой, но играбельной игры в нём нет —
|
|
31
26
|
* с причиной по каждой строке.
|
|
32
27
|
*/
|
|
33
|
-
export function pickActiveGame(manifests
|
|
34
|
-
const active =
|
|
35
|
-
? manifests.find(
|
|
36
|
-
manifest => manifest.id === gameId && isGameAvailable(manifest),
|
|
37
|
-
)
|
|
38
|
-
: manifests.find(isGameAvailable);
|
|
28
|
+
export function pickActiveGame(manifests) {
|
|
29
|
+
const active = manifests.find(isGameAvailable);
|
|
39
30
|
|
|
40
31
|
if (active) {
|
|
41
32
|
return active;
|
|
@@ -19,15 +19,6 @@ export class NetworkFailure extends Error {
|
|
|
19
19
|
|
|
20
20
|
export const isNetworkFailure = error => error instanceof NetworkFailure;
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
* Оборачивает отказ чтения тела ответа: SyntaxError — это мусор в JSON,
|
|
24
|
-
* любой другой отказ (TypeError обрыва) — сбой сети.
|
|
25
|
-
* @param {Error} error
|
|
26
|
-
* @returns {Error}
|
|
27
|
-
*/
|
|
28
|
-
export const bodyReadFailure = error =>
|
|
29
|
-
error instanceof SyntaxError ? error : new NetworkFailure(error);
|
|
30
|
-
|
|
31
22
|
/**
|
|
32
23
|
* @param {Function} doFetch - (...args) => Promise<Response>: сам fetch.
|
|
33
24
|
* @param {*} url
|
package/src/client/main.js
CHANGED
|
@@ -35,11 +35,10 @@ import {
|
|
|
35
35
|
bindLiveErrors,
|
|
36
36
|
} from './lib/formBuilder.js';
|
|
37
37
|
import { normalizeAuthParams } from './lib/authParams.js';
|
|
38
|
-
import { renderDocsLink } from './lib/footerLink.js';
|
|
39
38
|
import { createGameActivator } from './lib/gameActivator.js';
|
|
40
39
|
import createAutostart from './lib/autostart.js';
|
|
41
40
|
import { applyCatalogState } from './lib/catalogState.js';
|
|
42
|
-
import {
|
|
41
|
+
import { resolveBootConfig } from './boot.js';
|
|
43
42
|
import {
|
|
44
43
|
ensureGameShell,
|
|
45
44
|
ensureCanvas,
|
|
@@ -67,7 +66,6 @@ import { validateAuth } from '../lib/validators.js';
|
|
|
67
66
|
import SoundManager from './SoundManager.js';
|
|
68
67
|
import SignalingClient from './network/SignalingClient.js';
|
|
69
68
|
import LoopbackTransport from './network/LoopbackTransport.js';
|
|
70
|
-
import WebSocketTransport from './network/WebSocketTransport.js';
|
|
71
69
|
import InlineHostBridge from './network/InlineHostBridge.js';
|
|
72
70
|
import SessionSupervisor, {
|
|
73
71
|
SESSION_STATES,
|
|
@@ -83,11 +81,7 @@ import {
|
|
|
83
81
|
import { createVerifier, challengeOf } from './lib/pkce.js';
|
|
84
82
|
import { createTurnstile } from './lib/turnstile.js';
|
|
85
83
|
import TokenRenewer from './network/TokenRenewer.js';
|
|
86
|
-
import {
|
|
87
|
-
isKickClose,
|
|
88
|
-
POLICY_CLOSE_INFORMS,
|
|
89
|
-
shouldReloadAfterClose,
|
|
90
|
-
} from './network/policyClose.js';
|
|
84
|
+
import { isKickClose } from './network/policyClose.js';
|
|
91
85
|
import LobbyModel from './components/model/Lobby.js';
|
|
92
86
|
import LobbyView from './components/view/Lobby.js';
|
|
93
87
|
import LobbyCtrl from './components/controller/Lobby.js';
|
|
@@ -145,8 +139,8 @@ import runHotTick from './lib/hotTick.js';
|
|
|
145
139
|
|
|
146
140
|
// Динамическая загрузка игры по каталогу мастера (Этап 6.3): ClientPlugin
|
|
147
141
|
// (parts, bakers, игровой CSS, хуки ядра) грузится по entries.client манифеста
|
|
148
|
-
// — движок не импортирует игру статически. Первой активируется
|
|
149
|
-
//
|
|
142
|
+
// — движок не импортирует игру статически. Первой активируется первая
|
|
143
|
+
// доступная игра каталога, но активная игра не заморожена: выбор в #lobby-game и
|
|
150
144
|
// вход в чужую комнату переключают её через gameActivator (см. bindActiveGame).
|
|
151
145
|
// Переключение безопасно ровно до старта матча: всё пер-игровое состояние
|
|
152
146
|
// (Factory, Pixi-приложения, clientCore, звук) появляется только в CONFIG_DATA,
|
|
@@ -179,37 +173,24 @@ function bindActiveGame(manifest, plugin) {
|
|
|
179
173
|
}
|
|
180
174
|
|
|
181
175
|
// режим загрузки (Этап 2 плана standalone-sdk): lobby — прод с мастером,
|
|
182
|
-
// solo — хост в этой же вкладке (standalone SDK)
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
// каталог манифестов нужен только лобби-контуру, а его запрос не зависит от
|
|
187
|
-
// ответа /config — пускаем оба в полёт разом, иначе старт лобби платит лишний
|
|
188
|
-
// последовательный round-trip
|
|
189
|
-
const injectedBoot = getBootConfig();
|
|
190
|
-
const manifestPromise = injectedBoot
|
|
191
|
-
? null
|
|
192
|
-
: fetchGamesManifest(lobbyConfig.gamesManifestUrl, (...args) =>
|
|
193
|
-
fetchRetrying(fetch, ...args),
|
|
194
|
-
).catch(err => err);
|
|
195
|
-
|
|
196
|
-
let boot;
|
|
197
|
-
|
|
198
|
-
try {
|
|
199
|
-
boot = injectedBoot ?? (await resolveBootConfig());
|
|
200
|
-
} catch (e) {
|
|
201
|
-
// сети нет и режим неизвестен: репортёр ещё не создан (ему нужен режим),
|
|
202
|
-
// игроку остаётся надпись отказа
|
|
203
|
-
showBootFailure(`Failed to load the game: ${e.message}`);
|
|
204
|
-
throw e;
|
|
205
|
-
}
|
|
206
|
-
|
|
176
|
+
// solo — хост в этой же вкладке (standalone SDK). Ветвлений ровно пять:
|
|
177
|
+
// манифест, сигналинг/лобби, транспорт, авто-аутентификация и точка
|
|
178
|
+
// монтирования канвасов.
|
|
179
|
+
const boot = resolveBootConfig();
|
|
207
180
|
const bootMode = boot.mode;
|
|
208
181
|
const isLobbyMode = bootMode === 'lobby';
|
|
209
182
|
|
|
183
|
+
// каталог манифестов нужен только лобби-контуру: запрос уходит до создания
|
|
184
|
+
// журнала и каркаса, а ждут его только в загрузке игры ниже
|
|
185
|
+
const manifestPromise = isLobbyMode
|
|
186
|
+
? fetchGamesManifest(lobbyConfig.gamesManifestUrl, (...args) =>
|
|
187
|
+
fetchRetrying(fetch, ...args),
|
|
188
|
+
).catch(err => err)
|
|
189
|
+
: null;
|
|
190
|
+
|
|
210
191
|
// журнал клиентских ошибок: ставится как можно
|
|
211
|
-
// раньше, чтобы ловить и сбои самого старта. Лобби
|
|
212
|
-
//
|
|
192
|
+
// раньше, чтобы ловить и сбои самого старта. Лобби шлёт на свой бокс;
|
|
193
|
+
// SDK (solo) — только если встраивающий передал reportUrl
|
|
213
194
|
const diagnostics = createDiagnostics({
|
|
214
195
|
url:
|
|
215
196
|
bootMode === 'solo'
|
|
@@ -258,7 +239,7 @@ try {
|
|
|
258
239
|
|
|
259
240
|
try {
|
|
260
241
|
// недоступная игра активной быть не может (lib/pickActiveGame.js)
|
|
261
|
-
activeGameManifest = pickActiveGame(gamesManifest
|
|
242
|
+
activeGameManifest = pickActiveGame(gamesManifest);
|
|
262
243
|
syncDiagnosticsGame();
|
|
263
244
|
} catch (e) {
|
|
264
245
|
// «каталог непустой, но играбельного в нём нет» (движок обновили, все
|
|
@@ -266,7 +247,7 @@ try {
|
|
|
266
247
|
// лобби это то же самое, что пустой каталог: играть не во что, но
|
|
267
248
|
// модерация и заявка на месте, и только они это чинят. Терять их из-за
|
|
268
249
|
// такого нельзя, поэтому причина едет строкой отказа лобби, а не
|
|
269
|
-
// терминальным оверлеем. В solo
|
|
250
|
+
// терминальным оверлеем. В solo отказ остаётся отказом
|
|
270
251
|
if (!isLobbyMode) {
|
|
271
252
|
throw e;
|
|
272
253
|
}
|
|
@@ -278,7 +259,7 @@ try {
|
|
|
278
259
|
// пустой каталог — состояние лобби, а не отказ загрузки: модератор вправе
|
|
279
260
|
// отключить последнюю игру, реестр вправе ещё ничего не одобрить, и вернуть
|
|
280
261
|
// каталог к жизни можно только из панели, которая живёт в этой же вкладке.
|
|
281
|
-
// В solo
|
|
262
|
+
// В solo игры нет — это отказ: там весь смысл вкладки в матче
|
|
282
263
|
if (!activeGameManifest && !isLobbyMode) {
|
|
283
264
|
throw new Error('master has no games in its catalog');
|
|
284
265
|
}
|
|
@@ -338,7 +319,7 @@ const PC_LEAVE = wsports.client.LEAVE;
|
|
|
338
319
|
|
|
339
320
|
// сигнальный WebSocket мастера (лобби + установка P2P); игровой трафик идёт
|
|
340
321
|
// по WebRTC (transport), не через мастер
|
|
341
|
-
// (только lobby: solo
|
|
322
|
+
// (только lobby: у solo мастера нет вовсе)
|
|
342
323
|
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
343
324
|
const signaling = isLobbyMode
|
|
344
325
|
? new SignalingClient(`${wsProtocol}//${location.host}/`)
|
|
@@ -346,7 +327,7 @@ const signaling = isLobbyMode
|
|
|
346
327
|
|
|
347
328
|
// супервизор сессии (host-migration этап 4): держит текущий транспорт к
|
|
348
329
|
// хосту и переживает его обрыв — переподключается и возвращает место в
|
|
349
|
-
// матче. Создаётся при входе в комнату (или в solo
|
|
330
|
+
// матче. Создаётся при входе в комнату (или в solo-матч)
|
|
350
331
|
let supervisor = null;
|
|
351
332
|
|
|
352
333
|
// оверлей «Reconnecting…» на время переподключения (только лобби-режим)
|
|
@@ -406,7 +387,7 @@ const GAME_ROUND_START_CODE = GAME_CODES.roundStart[0];
|
|
|
406
387
|
// соединения важнее общего сообщения handleDisconnect
|
|
407
388
|
let terminalInformShown = false;
|
|
408
389
|
// ключ последнего терминального tech-кода: по нему лобби-режим отличает кик
|
|
409
|
-
// (policyClose.isKickClose) от закрытия комнаты —
|
|
390
|
+
// (policyClose.isKickClose) от закрытия комнаты — у транспортов кода закрытия нет
|
|
410
391
|
let terminalTechKey = null;
|
|
411
392
|
// снятие предыдущего animationend-листенера логотипа при повторном
|
|
412
393
|
// playLogoRoundStart — иначе листенеры копятся при частых стартах раунда
|
|
@@ -618,9 +599,9 @@ socketMethods[PS_AUTH_DATA] = data => {
|
|
|
618
599
|
|
|
619
600
|
document.getElementById('logo').textContent = texts?.title || 'VIMP';
|
|
620
601
|
|
|
621
|
-
// футер формы входа: версия пакета
|
|
622
|
-
//
|
|
623
|
-
//
|
|
602
|
+
// футер формы входа: версия пакета ИГРЫ. Здесь, а не в AuthView: его
|
|
603
|
+
// elems приходят из authSchema.elems игрового плагина, и футер каркаса —
|
|
604
|
+
// не его забота.
|
|
624
605
|
// Версию кладёт в манифест мастер (GameCatalog читает package.json
|
|
625
606
|
// пакета); в standalone-манифесте её может не быть — тогда ячейка пустая,
|
|
626
607
|
// раскладку space-between это не ломает
|
|
@@ -630,8 +611,6 @@ socketMethods[PS_AUTH_DATA] = data => {
|
|
|
630
611
|
authVersion.textContent = activeGameManifest?.packageVersion ?? '';
|
|
631
612
|
}
|
|
632
613
|
|
|
633
|
-
renderDocsLink(document.getElementById('auth-docs-link'), isDevBuild);
|
|
634
|
-
|
|
635
614
|
// память клиента (localStorage) + принудительное значение поля с
|
|
636
615
|
// единственным вариантом. Здесь, а не в AuthView: solo-путь ниже отвечает
|
|
637
616
|
// хосту вообще без формы и обязан прийти к тем же значениям
|
|
@@ -987,7 +966,7 @@ socketMethods[PS_GAME_INFORM_DATA] = data => {
|
|
|
987
966
|
|
|
988
967
|
// проигрывает shimmer-волну по логотипу в начале раунда; логотип живёт вне
|
|
989
968
|
// PanelView.containerId, поэтому панель им не управляет
|
|
990
|
-
// (см. docs
|
|
969
|
+
// (см. VIMP docs: client-api.md)
|
|
991
970
|
const playLogoRoundStart = () => {
|
|
992
971
|
const logo = document.getElementById('logo');
|
|
993
972
|
|
|
@@ -1672,10 +1651,8 @@ function handleMessage(data) {
|
|
|
1672
1651
|
|
|
1673
1652
|
// терминальное закрытие сессии (супервизор: возвращаться некуда — кик,
|
|
1674
1653
|
// политика, уход хоста, окно переподключения истекло). Останавливаем
|
|
1675
|
-
// рендер, показываем заглушку и возвращаемся в лобби
|
|
1676
|
-
|
|
1677
|
-
// транспорты эмитят close без него
|
|
1678
|
-
function handleDisconnect(closeCode) {
|
|
1654
|
+
// рендер, показываем заглушку и возвращаемся в лобби перезагрузкой
|
|
1655
|
+
function handleDisconnect() {
|
|
1679
1656
|
showSessionOverlay(null);
|
|
1680
1657
|
|
|
1681
1658
|
// app.stop() здесь не зовём: при sharedTicker это Ticker.shared.stop()
|
|
@@ -1729,31 +1706,18 @@ function handleDisconnect(closeCode) {
|
|
|
1729
1706
|
inlineHost?.destroy();
|
|
1730
1707
|
inlineHost = null;
|
|
1731
1708
|
|
|
1732
|
-
//
|
|
1733
|
-
//
|
|
1734
|
-
// не пишется вообще ничего, и побеждает серверный текст
|
|
1735
|
-
const policyInform = POLICY_CLOSE_INFORMS[closeCode];
|
|
1736
|
-
|
|
1737
|
-
// терминальную причину закрытия (кик, полная комната) не затираем
|
|
1709
|
+
// терминальную причину закрытия (кик, полная комната — TECH_INFORM перед
|
|
1710
|
+
// закрытием) не затираем
|
|
1738
1711
|
if (!terminalInformShown) {
|
|
1739
1712
|
socketMethods[PS_TECH_INFORM_DATA](
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
: 'The match is over — connection to the host is closed.'),
|
|
1713
|
+
isLobbyMode
|
|
1714
|
+
? 'Host left — the room is closed. Finding another room…'
|
|
1715
|
+
: 'The match is over — connection to the host is closed.',
|
|
1744
1716
|
);
|
|
1745
1717
|
}
|
|
1746
1718
|
|
|
1747
|
-
// в solo перезагружаться некуда: лобби нет, а матч поднимается с
|
|
1748
|
-
// В dedicated сервер жив — переподключение перезагрузкой уместно, но не
|
|
1749
|
-
// тогда, когда он сам нас и отбил по политике (network/policyClose.js)
|
|
1750
|
-
if (bootMode === 'solo' || !shouldReloadAfterClose(closeCode)) {
|
|
1751
|
-
return;
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1719
|
+
// в solo перезагружаться некуда: лобби нет, а матч поднимается с нуля
|
|
1754
1720
|
if (!isLobbyMode) {
|
|
1755
|
-
setTimeout(() => reloadPage(), 3000);
|
|
1756
|
-
|
|
1757
1721
|
return;
|
|
1758
1722
|
}
|
|
1759
1723
|
|
|
@@ -1763,7 +1727,7 @@ function handleDisconnect(closeCode) {
|
|
|
1763
1727
|
// — на главную без перезагрузки (причина остаётся в #tech-informer, клик
|
|
1764
1728
|
// по нему открывает лобби); закрытие комнаты — в быструю игру той же игры
|
|
1765
1729
|
const exit = decideExitRoute({
|
|
1766
|
-
kicked: isKickClose(
|
|
1730
|
+
kicked: isKickClose(terminalTechKey) || roomCtx.startFailed,
|
|
1767
1731
|
gameId: activeGameManifest?.id,
|
|
1768
1732
|
});
|
|
1769
1733
|
|
|
@@ -1803,10 +1767,9 @@ function startSession(transport, reconnect = null) {
|
|
|
1803
1767
|
onResumeRejected: handleResumeRejected,
|
|
1804
1768
|
onHostLost: reportHostUnreachable,
|
|
1805
1769
|
onColdRestart: handleColdRestart,
|
|
1806
|
-
// кик (TECH_INFORM перед закрытием)
|
|
1770
|
+
// кик и полная комната (TECH_INFORM перед закрытием) не лечатся
|
|
1807
1771
|
// переподключением
|
|
1808
|
-
isTerminalClose:
|
|
1809
|
-
terminalInformShown || !shouldReloadAfterClose(closeCode),
|
|
1772
|
+
isTerminalClose: () => terminalInformShown,
|
|
1810
1773
|
reconnect,
|
|
1811
1774
|
timing: lobbyConfig.session,
|
|
1812
1775
|
});
|
|
@@ -2221,14 +2184,6 @@ async function connectSolo() {
|
|
|
2221
2184
|
transport.connect();
|
|
2222
2185
|
}
|
|
2223
2186
|
|
|
2224
|
-
// dedicated-режим: прямой WebSocket к Node-серверу игры
|
|
2225
|
-
function connectDedicated() {
|
|
2226
|
-
const transport = new WebSocketTransport(boot.wsUrl);
|
|
2227
|
-
|
|
2228
|
-
startSession(transport);
|
|
2229
|
-
transport.connect();
|
|
2230
|
-
}
|
|
2231
|
-
|
|
2232
2187
|
// REST-запрос списка серверов у мастера (поиск игнорирует пагинацию)
|
|
2233
2188
|
async function fetchServers({ offset, limit, search }) {
|
|
2234
2189
|
const params = new URLSearchParams();
|
|
@@ -2317,7 +2272,7 @@ let roomFormFields = new Map();
|
|
|
2317
2272
|
let roomFormDescriptors = [];
|
|
2318
2273
|
|
|
2319
2274
|
// генерирует форму создания комнаты по явной схеме манифеста (roomForm,
|
|
2320
|
-
// docs
|
|
2275
|
+
// VIMP docs: plugin-api.md § "Form schema") — движок не выводит контролы
|
|
2321
2276
|
// из типа значения, схема плагина полностью описывает форму
|
|
2322
2277
|
function populateRoomForm(manifest) {
|
|
2323
2278
|
const container = document.getElementById(lobbyConfig.elems.fieldsId);
|
|
@@ -2705,7 +2660,7 @@ function initLobby() {
|
|
|
2705
2660
|
// валидация (pattern/required/min/max) — единственная граница
|
|
2706
2661
|
// room-формы: она едет клиенту как JSON манифеста, JS-валидаторы
|
|
2707
2662
|
// (как в auth-форме) туда не сериализуются
|
|
2708
|
-
// (docs
|
|
2663
|
+
// (VIMP docs: plugin-api.md § "Form schema"); авторитетный клампинг
|
|
2709
2664
|
// всё равно в applyRoomOverrides.js (вызывается из host.worker.js при
|
|
2710
2665
|
// создании комнаты). Проверяем до активации игры: неверная форма не должна
|
|
2711
2666
|
// стоить загрузки плагина. arm() и рисует ошибки, и снимает фильтр «только
|
|
@@ -2813,7 +2768,7 @@ function leaveRoomWith(message) {
|
|
|
2813
2768
|
}
|
|
2814
2769
|
|
|
2815
2770
|
// точка 2 ветвления: лобби, экран профиля и сигналинг живут только в lobby;
|
|
2816
|
-
// solo
|
|
2771
|
+
// solo идёт сразу к транспорту
|
|
2817
2772
|
if (isLobbyMode) {
|
|
2818
2773
|
authApi = createAuthApi({ serviceUrl: authClientConfig.serviceUrl });
|
|
2819
2774
|
|
|
@@ -2956,7 +2911,7 @@ if (isLobbyMode) {
|
|
|
2956
2911
|
window.addEventListener('hashchange', () => route.handleHashChange());
|
|
2957
2912
|
|
|
2958
2913
|
signaling.connect();
|
|
2959
|
-
} else
|
|
2914
|
+
} else {
|
|
2960
2915
|
try {
|
|
2961
2916
|
await connectSolo();
|
|
2962
2917
|
} catch (e) {
|
|
@@ -2965,6 +2920,4 @@ if (isLobbyMode) {
|
|
|
2965
2920
|
);
|
|
2966
2921
|
throw e;
|
|
2967
2922
|
}
|
|
2968
|
-
} else {
|
|
2969
|
-
connectDedicated();
|
|
2970
2923
|
}
|