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.
Files changed (45) hide show
  1. package/README.md +2 -2
  2. package/bin/vimp-sim.js +2 -2
  3. package/core/Cargo.toml +2 -2
  4. package/package.json +3 -4
  5. package/src/client/boot.js +11 -87
  6. package/src/client/components/model/RoomMenu.js +1 -1
  7. package/src/client/components/view/Auth.js +3 -3
  8. package/src/client/lib/formBuilder.js +7 -6
  9. package/src/client/lib/hostVoteCommand.js +2 -2
  10. package/src/client/lib/pickActiveGame.js +3 -12
  11. package/src/client/lib/retryOnNetworkError.js +0 -9
  12. package/src/client/main.js +43 -90
  13. package/src/client/network/SessionSupervisor.js +14 -14
  14. package/src/client/network/WebRtcManager.js +1 -2
  15. package/src/client/network/policyClose.js +10 -77
  16. package/src/client/style.css +4 -6
  17. package/src/client/views/gameShell.js +2 -4
  18. package/src/client/views/includes/auth.pug +1 -4
  19. package/src/client/views/includes/lobby.pug +3 -6
  20. package/src/config/closeCodes.js +7 -9
  21. package/src/config/gameCodes.js +1 -1
  22. package/src/config/hostDefaults.js +2 -2
  23. package/src/config/lobby.js +3 -4
  24. package/src/config/wsports.js +1 -1
  25. package/src/devtools/ScenarioRunner.js +1 -1
  26. package/src/devtools/builtinScenario.js +2 -2
  27. package/src/devtools/contract/rules/a2-package-scripts.js +1 -1
  28. package/src/devtools/contract/rules/a4-vite-config.js +1 -1
  29. package/src/devtools/contract/rules/c4-component-dependencies.js +1 -1
  30. package/src/devtools/invariants.js +1 -1
  31. package/src/devtools/pluginLoader.js +2 -2
  32. package/src/host/HostGame.js +4 -4
  33. package/src/host/PortMachine.js +4 -21
  34. package/src/host/commandFailure.js +2 -2
  35. package/src/host/identity.js +3 -3
  36. package/src/host/meta/core/CommandProcessor.js +2 -2
  37. package/src/host/meta/modules/Accolades.js +1 -1
  38. package/src/lib/createHostRuntime.js +1 -1
  39. package/src/lib/loadGamePackage.js +7 -9
  40. package/src/lib/offlinePlayerData.js +1 -1
  41. package/src/lib/validators.js +2 -2
  42. package/src/lib/voicePacket.js +1 -1
  43. package/src/standalone/index.js +3 -2
  44. package/src/client/lib/footerLink.js +0 -30
  45. 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
- [plugin contract](https://docs.vimp.lgick.dev/en/plugin-api).
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
- Documentation: https://docs.vimp.lgick.dev
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. See § Scenario format at\n' +
44
- 'notice: https://docs.vimp.lgick.dev/en/debugging#scenario-format, then run\n' +
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.4"
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://docs.vimp.lgick.dev/en/core"
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.39.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://docs.vimp.lgick.dev",
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/dedicated -w src/lib -w src/config",
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",
@@ -1,33 +1,19 @@
1
1
  // Режим загрузки клиента движка (Этап 2 плана standalone-sdk). Клиент один
2
- // на все контуры, а различает их этот модуль:
2
+ // на оба контура, а различает их этот модуль:
3
3
  //
4
- // lobby — прод: каталог игр и сигналинг мастера, лобби за экраном профиля;
5
- // solo — standalone SDK: хост в этой же вкладке (inline, без Worker'а),
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' | 'dedicated',
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 → GET /config → лобби.
73
- * @param {Function} [fetchImpl] - Реализация fetch (тесты подставляют свою).
74
- * @param {Object} [retry] - Параметры fetchRetrying (тесты отключают паузы).
75
- * @returns {Promise<Object>} Конфиг загрузки; отклоняется NetworkFailure, если
76
- * сеть не ответила после повторов.
56
+ * Разрешает режим загрузки: инъекция SDK, иначе лобби.
57
+ * @returns {Object} Конфиг загрузки.
77
58
  */
78
- export async function resolveBootConfig(fetchImpl, retry) {
79
- if (bootConfig) {
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/dedicated ссылки на комнату нет
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.vimp.lgick.dev/en/plugin-api "Form schema"), едет по сети в
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, docs.vimp.lgick.dev/en/plugin-api "Form
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
- // контракт дескрипторов — docs.vimp.lgick.dev/en/plugin-api, раздел "Form
8
- // schema"). control: 'select'|'text'|'checkbox'|'radio'; все контролы —
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
- // строит одно поле формы по дескриптору (docs.vimp.lgick.dev/en/plugin-api,
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.vimp.lgick.dev/en/plugin-api "Form schema": roomDefaults остаётся
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/dedicated мастера нет — команда остаётся обычным
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' | 'dedicated'.
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, gameId) {
34
- const active = gameId
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
@@ -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 { getBootConfig, resolveBootConfig } from './boot.js';
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
- // — движок не импортирует игру статически. Первой активируется gamesManifest[0]
149
- // (или boot.gameId), но активная игра не заморожена: выбор в #lobby-game и
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), dedicated — прямой WS к
183
- // Node-серверу. Ветвлений ровно пять: манифест, сигналинг/лобби, транспорт,
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
- // раньше, чтобы ловить и сбои самого старта. Лобби и dedicated шлют на
212
- // свой бокс; SDK (solo) — только если встраивающий передал reportUrl
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, boot.gameId);
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/dedicated отказ остаётся отказом
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/dedicated игры нет — это отказ: там весь смысл вкладки в матче
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 и dedicated мастера не имеют вовсе)
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/dedicated-матч)
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) от закрытия комнаты — в P2P кода закрытия нет
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
- // футер формы входа: версия пакета ИГРЫ и (в dev-сборке) ссылка на
622
- // документацию. Здесь, а не в AuthView: его elems приходят из
623
- // authSchema.elems игрового плагина, и футер каркаса — не его забота.
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.vimp.lgick.dev/en/client-api)
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
- // closeCode приходит только от WebSocket-транспорта (dedicated), остальные
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
- // (TECH_INFORM перед закрытием — так приезжает 4006, полная комната), ниже
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
- policyInform ??
1741
- (isLobbyMode
1742
- ? 'Host left — the room is closed. Finding another room…'
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(closeCode, terminalTechKey) || roomCtx.startFailed,
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: closeCode =>
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.vimp.lgick.dev/en/plugin-api "Form schema") — движок не выводит контролы
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.vimp.lgick.dev/en/plugin-api "Form schema"); авторитетный клампинг
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 и dedicated идут сразу к транспорту
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 if (bootMode === 'solo') {
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
  }