vimp-engine 0.38.1 → 0.38.3

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 (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +12 -6
  3. package/bin/vimp-sim.js +2 -2
  4. package/core/Cargo.toml +4 -3
  5. package/package.json +14 -5
  6. package/src/client/boot.js +30 -7
  7. package/src/client/components/controller/ClientReports.js +1 -1
  8. package/src/client/components/controller/Games.js +1 -1
  9. package/src/client/components/model/ClientReports.js +1 -1
  10. package/src/client/components/model/Games.js +24 -0
  11. package/src/client/components/view/Auth.js +4 -2
  12. package/src/client/components/view/ClientReports.js +1 -1
  13. package/src/client/components/view/Games.js +19 -1
  14. package/src/client/components/view/Lobby.js +1 -7
  15. package/src/client/debug.js +1 -1
  16. package/src/client/lib/authApi.js +1 -1
  17. package/src/client/lib/diagnostics.js +19 -4
  18. package/src/client/lib/engineVersion.js +0 -6
  19. package/src/client/lib/footerLink.js +15 -14
  20. package/src/client/lib/formBuilder.js +9 -8
  21. package/src/client/lib/retryOnNetworkError.js +60 -0
  22. package/src/client/main.js +46 -28
  23. package/src/client/network/HiddenHostHealthLog.js +40 -3
  24. package/src/client/network/HostController.js +4 -4
  25. package/src/client/network/HostHealthPolicy.js +41 -0
  26. package/src/client/network/InlineHostBridge.js +8 -1
  27. package/src/client/network/WebSocketTransport.js +1 -1
  28. package/src/client/session/HandoffFlow.js +5 -0
  29. package/src/client/session/HostRole.js +5 -2
  30. package/src/client/style.css +5 -6
  31. package/src/client/views/gameShell.js +3 -3
  32. package/src/client/views/includes/auth.pug +4 -4
  33. package/src/client/views/includes/lobby.pug +6 -5
  34. package/src/client/views/includes/reports.pug +1 -1
  35. package/src/config/authClient.js +4 -5
  36. package/src/config/gameCodes.js +2 -2
  37. package/src/config/lobby.js +4 -5
  38. package/src/config/opcodes.js +2 -1
  39. package/src/config/wsports.js +2 -2
  40. package/src/devtools/builtinScenario.js +3 -3
  41. package/src/devtools/contract/loadContext.js +2 -2
  42. package/src/devtools/contract/rules/a2-package-scripts.js +4 -3
  43. package/src/devtools/contract/rules/a4-vite-config.js +4 -3
  44. package/src/devtools/contract/rules/a7-package-repository.js +7 -7
  45. package/src/devtools/contract/rules/c4-component-dependencies.js +5 -5
  46. package/src/devtools/contract/rules/index.js +1 -1
  47. package/src/devtools/invariants.js +2 -2
  48. package/src/devtools/pluginLoader.js +3 -3
  49. package/src/host/DebugRecorder.js +1 -1
  50. package/src/host/HostGame.js +35 -15
  51. package/src/host/PortMachine.js +6 -5
  52. package/src/host/commandFailure.js +140 -0
  53. package/src/host/host.worker.js +8 -2
  54. package/src/host/meta/SocketManager.js +2 -2
  55. package/src/host/meta/core/CommandProcessor.js +37 -7
  56. package/src/lib/applyRoomOverrides.js +1 -1
  57. package/src/lib/capabilities.js +1 -1
  58. package/src/lib/checkpointCodec.js +1 -2
  59. package/src/lib/coreConfig.js +1 -1
  60. package/src/lib/createHostRuntime.js +1 -1
  61. package/src/lib/gameId.js +1 -1
  62. package/src/lib/gamePlugin.js +6 -2
  63. package/src/lib/jwt.js +1 -1
  64. package/src/lib/loadGamePackage.js +1 -1
  65. package/src/lib/packageLink.js +21 -63
  66. package/src/lib/validators.js +3 -3
  67. package/src/standalone/index.js +6 -6
  68. package/tests/fixtures/generations/gen-api3/README.md +2 -2
  69. package/tests/fixtures/generations/gen-api4/README.md +2 -2
  70. package/bin/vimp-surface.js +0 -88
  71. package/src/config/env.js +0 -104
  72. package/src/config/master.js +0 -288
  73. package/src/devtools/surface/abiParse.js +0 -175
  74. package/src/devtools/surface/collect.js +0 -413
  75. package/src/lib/clientIp.js +0 -96
  76. package/src/lib/rateLimiter.js +0 -40
  77. package/src/lib/security.js +0 -45
@@ -32,7 +32,7 @@ import {
32
32
  bindLiveErrors,
33
33
  } from './lib/formBuilder.js';
34
34
  import { normalizeAuthParams } from './lib/authParams.js';
35
- import { renderProjectLink } from './lib/footerLink.js';
35
+ import { renderDocsLink } from './lib/footerLink.js';
36
36
  import { createGameActivator } from './lib/gameActivator.js';
37
37
  import createAutostart from './lib/autostart.js';
38
38
  import { applyCatalogState } from './lib/catalogState.js';
@@ -46,6 +46,7 @@ import { createContextTracker } from './lib/contextTracker.js';
46
46
  import { createLocalPlayer } from './lib/localPlayer.js';
47
47
  import { createAccolades } from './lib/accolades.js';
48
48
  import { createDiagnostics } from './lib/diagnostics.js';
49
+ import { fetchRetrying, isNetworkFailure } from './lib/retryOnNetworkError.js';
49
50
  import { dispatchSocketMessage } from './lib/socketDispatch.js';
50
51
  import { pickActiveGame, isGameAvailable } from './lib/pickActiveGame.js';
51
52
  import { readCoreAbi, dispatchCoreOp, ABI_UNKNOWN } from '../lib/coreAbi.js';
@@ -183,13 +184,25 @@ function bindActiveGame(manifest, plugin) {
183
184
  const injectedBoot = getBootConfig();
184
185
  const manifestPromise = injectedBoot
185
186
  ? null
186
- : fetchGamesManifest(lobbyConfig.gamesManifestUrl).catch(err => err);
187
+ : fetchGamesManifest(lobbyConfig.gamesManifestUrl, (...args) =>
188
+ fetchRetrying(fetch, ...args),
189
+ ).catch(err => err);
190
+
191
+ let boot;
192
+
193
+ try {
194
+ boot = injectedBoot ?? (await resolveBootConfig());
195
+ } catch (e) {
196
+ // сети нет и режим неизвестен: репортёр ещё не создан (ему нужен режим),
197
+ // игроку остаётся надпись отказа
198
+ showBootFailure(`Failed to load the game: ${e.message}`);
199
+ throw e;
200
+ }
187
201
 
188
- const boot = injectedBoot ?? (await resolveBootConfig());
189
202
  const bootMode = boot.mode;
190
203
  const isLobbyMode = bootMode === 'lobby';
191
204
 
192
- // журнал клиентских ошибок (plan/client-reports): ставится как можно
205
+ // журнал клиентских ошибок: ставится как можно
193
206
  // раньше, чтобы ловить и сбои самого старта. Лобби и dedicated шлют на
194
207
  // свой бокс; SDK (solo) — только если встраивающий передал reportUrl
195
208
  const diagnostics = createDiagnostics({
@@ -273,6 +286,12 @@ try {
273
286
  }
274
287
  } catch (e) {
275
288
  showBootFailure(`Failed to load the game: ${e.message}`, gameContainer);
289
+
290
+ // сбой сети — не ошибка кода: в журнал он не уходит
291
+ if (isNetworkFailure(e)) {
292
+ diagnostics.ignore(e);
293
+ }
294
+
276
295
  throw e;
277
296
  }
278
297
 
@@ -348,8 +367,8 @@ const renderContexts = {};
348
367
  // контекста (хост её повторно не пришлёт)
349
368
  let lastMapData = null;
350
369
 
351
- // dev-сборка (Vite подставляет константу): включает отладочный контур этапа 6
352
- // плана plan/done/ai-debug — рекордер в комнате и window.__vimpDebug. В прод-бандле
370
+ // dev-сборка (Vite подставляет константу): включает отладочный контур —
371
+ // рекордер в комнате и window.__vimpDebug. В прод-бандле
353
372
  // ветка вырезается сборкой, поведение не меняется
354
373
  const isDevBuild =
355
374
  typeof import.meta.env !== 'undefined' && import.meta.env.DEV === true;
@@ -527,7 +546,7 @@ socketMethods[PS_CONFIG_DATA] = async data => {
527
546
  // part, объявивший сервис в componentDependencies, строит URL сам —
528
547
  // движок не знает ни имён файлов, ни их раскладки внутри пакета
529
548
  assetsBase: activeGameManifest.assetsBase,
530
- // журнал клиентских ошибок для партов игры (plan/client-reports):
549
+ // журнал клиентских ошибок для партов игры:
531
550
  // warn(code, details) — своё предупреждение, capture(error) — пойманная
532
551
  // ошибка. Сервис опционален: игра не пишет его в requires
533
552
  diagnostics: {
@@ -577,21 +596,19 @@ socketMethods[PS_AUTH_DATA] = data => {
577
596
 
578
597
  document.getElementById('logo').textContent = texts?.title || 'VIMP';
579
598
 
580
- // футер формы входа: версия и ссылка пакета ИГРЫ. Здесь, а не в AuthView:
581
- // его elems приходят из authSchema.elems игрового плагина, и футер каркаса
582
- // — не его забота. Метаданные кладёт в манифест мастер (GameCatalog читает
583
- // package.json пакета); в standalone-манифесте их может не быть — тогда
584
- // ячейки пустые, раскладку space-between это не ломает
599
+ // футер формы входа: версия пакета ИГРЫ и (в dev-сборке) ссылка на
600
+ // документацию. Здесь, а не в AuthView: его elems приходят из
601
+ // authSchema.elems игрового плагина, и футер каркаса — не его забота.
602
+ // Версию кладёт в манифест мастер (GameCatalog читает package.json
603
+ // пакета); в standalone-манифесте её может не быть — тогда ячейка пустая,
604
+ // раскладку space-between это не ломает
585
605
  const authVersion = document.getElementById('auth-version');
586
606
 
587
607
  if (authVersion) {
588
608
  authVersion.textContent = activeGameManifest?.packageVersion ?? '';
589
609
  }
590
610
 
591
- renderProjectLink(
592
- document.getElementById('auth-package-link'),
593
- activeGameManifest?.packageUrl,
594
- );
611
+ renderDocsLink(document.getElementById('auth-docs-link'), isDevBuild);
595
612
 
596
613
  // память клиента (localStorage) + принудительное значение поля с
597
614
  // единственным вариантом. Здесь, а не в AuthView: solo-путь ниже отвечает
@@ -934,7 +951,8 @@ socketMethods[PS_GAME_INFORM_DATA] = data => {
934
951
  };
935
952
 
936
953
  // проигрывает shimmer-волну по логотипу в начале раунда; логотип живёт вне
937
- // PanelView.containerId, поэтому панель им не управляет (см. docs/en/client.md)
954
+ // PanelView.containerId, поэтому панель им не управляет
955
+ // (см. docs.vimp.lgick.dev/en/client-api)
938
956
  const playLogoRoundStart = () => {
939
957
  const logo = document.getElementById('logo');
940
958
 
@@ -1033,7 +1051,7 @@ socketMethods[PS_RESUME_RESULT] = data => {
1033
1051
  };
1034
1052
 
1035
1053
  // console: логи авторитетной половины (Worker изолирован от DevTools вкладки —
1036
- // иначе его события в браузере не видны вовсе, этап 6 плана plan/done/ai-debug)
1054
+ // иначе его события в браузере не видны вовсе)
1037
1055
  socketMethods[PS_CONSOLE] = data => {
1038
1056
  console.log(`${DEBUG_PREFIX}[host]`, data);
1039
1057
  };
@@ -2182,8 +2200,8 @@ let roomFormFields = new Map();
2182
2200
  let roomFormDescriptors = [];
2183
2201
 
2184
2202
  // генерирует форму создания комнаты по явной схеме манифеста (roomForm,
2185
- // docs/en/plugin-api.md "Form schema") — движок не выводит контролы из типа
2186
- // значения, схема плагина полностью описывает форму
2203
+ // docs.vimp.lgick.dev/en/plugin-api "Form schema") — движок не выводит контролы
2204
+ // из типа значения, схема плагина полностью описывает форму
2187
2205
  function populateRoomForm(manifest) {
2188
2206
  const container = document.getElementById(lobbyConfig.elems.fieldsId);
2189
2207
 
@@ -2477,7 +2495,7 @@ function initLobby() {
2477
2495
  // сам, по нику из VIMP_ADMIN_NICKS
2478
2496
  games.setAdmin(lobbyAuthModel.getRole() === 'admin');
2479
2497
 
2480
- // журнал клиентских ошибок (plan/client-reports, этап 5): кнопку «Errors»
2498
+ // журнал клиентских ошибок: кнопку «Errors»
2481
2499
  // видит только админ; фильтр по игре — каталог вкладки
2482
2500
  const clientReportsModel = new ClientReportsModel(
2483
2501
  lobbyConfig.clientReports,
@@ -2569,13 +2587,13 @@ function initLobby() {
2569
2587
  hostBtn?.addEventListener('click', async () => {
2570
2588
  // валидация (pattern/required/min/max) — единственная граница
2571
2589
  // room-формы: она едет клиенту как JSON манифеста, JS-валидаторы
2572
- // (как в auth-форме) туда не сериализуются (docs/en/plugin-api.md
2573
- // "Form schema"); авторитетный клампинг всё равно в applyRoomOverrides.js
2574
- // (вызывается из host.worker.js при создании комнаты). Проверяем до
2575
- // активации игры: неверная форма не должна стоить загрузки плагина.
2576
- // arm() и рисует ошибки, и снимает фильтр «только тронутые поля»: клик —
2577
- // это ответ за форму целиком, включая поля, которых игрок не касался.
2578
- // Он же чистит блок, когда ошибок нет
2590
+ // (как в auth-форме) туда не сериализуются
2591
+ // (docs.vimp.lgick.dev/en/plugin-api "Form schema"); авторитетный клампинг
2592
+ // всё равно в applyRoomOverrides.js (вызывается из host.worker.js при
2593
+ // создании комнаты). Проверяем до активации игры: неверная форма не должна
2594
+ // стоить загрузки плагина. arm() и рисует ошибки, и снимает фильтр «только
2595
+ // тронутые поля»: клик — это ответ за форму целиком, включая поля, которых
2596
+ // игрок не касался. Он же чистит блок, когда ошибок нет
2579
2597
  if (liveErrors.arm().length) {
2580
2598
  return;
2581
2599
  }
@@ -7,8 +7,14 @@
7
7
  // Эпизод закрывается возвратом видимости, потерей роли хоста или
8
8
  // выгрузкой страницы (flush). Следующие эпизоды той же сессии журнал
9
9
  // склеивает с первым: растёт счётчик, details остаются первого — осознанно.
10
- // Эпизод без единой метрики (матч стоял, скрытие короче окна цикла) не
11
- // пишется. Без DOM: видимость сообщает владелец.
10
+ // Пишется только эпизод, в котором матч просел (потеряно время или частота
11
+ // ниже порога), это задело удалённых игроков (peers > 0), а передача хоста
12
+ // не началась без видимой причины (handoff 'unexplained'): просадка без
13
+ // игроков, ожидаемый отказ (нет преемника, срок роли, кулдаун, выключено) и
14
+ // начатая передача — штатный троттлинг, а сводка здорового эпизода заняла бы
15
+ // место details первой записи. Эпизод без единой метрики (матч стоял,
16
+ // скрытие короче окна цикла) не пишется. Без DOM: видимость и исход
17
+ // передачи сообщает владелец.
12
18
 
13
19
  export const HIDDEN_HEALTH_CODE = 'engine.host.hiddenHealth';
14
20
 
@@ -16,11 +22,22 @@ export default class HiddenHostHealthLog {
16
22
  /**
17
23
  * @param {Object} options
18
24
  * @param {Function} options.warn - (code, summary) запись в журнал.
25
+ * @param {number} options.minHealthyTickRate - ниже — эпизод просел.
26
+ * @param {Function} options.handoffOutcome - () => исход автопередачи
27
+ * (HostHealthPolicy.hiddenHandoff).
19
28
  * @param {boolean} [options.hidden] - вкладка скрыта в момент создания.
20
29
  * @param {Function} [options.now] - монотонные часы, мс.
21
30
  */
22
- constructor({ warn, hidden = false, now = () => performance.now() }) {
31
+ constructor({
32
+ warn,
33
+ minHealthyTickRate,
34
+ handoffOutcome,
35
+ hidden = false,
36
+ now = () => performance.now(),
37
+ }) {
23
38
  this._warn = warn;
39
+ this._minHealthyTickRate = minHealthyTickRate;
40
+ this._handoffOutcome = handoffOutcome;
24
41
  this._now = now;
25
42
  this._episode = null;
26
43
 
@@ -35,6 +52,7 @@ export default class HiddenHostHealthLog {
35
52
  minTickRate: Infinity,
36
53
  maxGapMs: 0,
37
54
  lostMs: 0,
55
+ peers: 0,
38
56
  };
39
57
  } else if (!hidden) {
40
58
  this.flush();
@@ -53,6 +71,11 @@ export default class HiddenHostHealthLog {
53
71
  episode.minTickRate = Math.min(episode.minTickRate, health.tickRate);
54
72
  episode.maxGapMs = Math.max(episode.maxGapMs, health.maxGapMs);
55
73
  episode.lostMs += health.lostMs;
74
+
75
+ // peers — по просевшим метрикам: сколько игроков задела просадка
76
+ if (health.lostMs > 0 || health.tickRate < this._minHealthyTickRate) {
77
+ episode.peers = Math.max(episode.peers, health.peerCount ?? 0);
78
+ }
56
79
  }
57
80
 
58
81
  // закрыть эпизод (если идёт) и записать сводку
@@ -65,12 +88,26 @@ export default class HiddenHostHealthLog {
65
88
  return;
66
89
  }
67
90
 
91
+ // peers считается по просевшим метрикам: 0 — здоровый эпизод либо
92
+ // просадка без удалённых игроков
93
+ if (episode.peers === 0) {
94
+ return;
95
+ }
96
+
97
+ const handoff = this._handoffOutcome();
98
+
99
+ if (handoff !== 'unexplained') {
100
+ return;
101
+ }
102
+
68
103
  this._warn(HIDDEN_HEALTH_CODE, {
69
104
  hiddenMs: Math.round(this._now() - episode.startedAt),
70
105
  samples: episode.samples,
71
106
  minTickRate: episode.minTickRate,
72
107
  maxGapMs: episode.maxGapMs,
73
108
  lostMs: episode.lostMs,
109
+ peers: episode.peers,
110
+ handoff,
74
111
  });
75
112
  }
76
113
  }
@@ -56,7 +56,7 @@ export default class HostController {
56
56
  * @param {Function} [opts.onLobbyInfoChange] - смена строки карточки
57
57
  * комнаты в лобби (string|null) — для актуализации info у мастера.
58
58
  * @param {Object} [opts.diagnostics] - журнал клиентских ошибок
59
- * (lib/diagnostics.js, plan/client-reports): туда уходят ошибки Worker'а.
59
+ * (lib/diagnostics.js): туда уходят ошибки Worker'а.
60
60
  * @param {Uint8Array} [opts.checkpoint] - сжатая контрольная точка
61
61
  * (host-migration этап 5): Worker поднимает матч из неё на паузе, старт —
62
62
  * startAfterRestore(). Буфер передаётся списком переноса — после
@@ -114,7 +114,7 @@ export default class HostController {
114
114
  // подписчики метрик здоровья хоста (host-migration этап 9a)
115
115
  this._healthListeners = new Set();
116
116
 
117
- // отладочные запросы в Worker (этап 6 плана plan/done/ai-debug):
117
+ // отладочные запросы в Worker:
118
118
  // requestId → { resolve, reject }
119
119
  this._debugRequests = new Map();
120
120
  this._debugRequestId = 0;
@@ -149,7 +149,7 @@ export default class HostController {
149
149
  });
150
150
  }
151
151
 
152
- // журнал клиентских ошибок (plan/client-reports): неперехваченная ошибка
152
+ // журнал клиентских ошибок: неперехваченная ошибка
153
153
  // Worker'а приходит сюда. preventDefault не зовётся — консольный вывод
154
154
  // браузера остаётся как есть
155
155
  _watchWorkerErrors(worker) {
@@ -456,7 +456,7 @@ export default class HostController {
456
456
  }
457
457
 
458
458
  /**
459
- * Отладочный контур (этап 6 плана plan/done/ai-debug): начинает запись живого
459
+ * Отладочный контур: начинает запись живого
460
460
  * матча в формат сценария headless-runner'а.
461
461
  * @returns {Promise<boolean>} false — dev-режим в комнате выключен.
462
462
  */
@@ -56,6 +56,7 @@ export default class HostHealthPolicy {
56
56
  this._successor = false;
57
57
  this._hiddenSince = null;
58
58
  this._hiddenTimer = null;
59
+ this._hiddenOutcome = null;
59
60
  this._lastAutoAt = null;
60
61
  this._cooldownBefore = null;
61
62
  this._samples = [];
@@ -80,6 +81,12 @@ export default class HostHealthPolicy {
80
81
  this._successor = false;
81
82
  this._resetSamples();
82
83
 
84
+ // исход прошлой роли уже прочитан её журналом (flush при потере роли);
85
+ // новой роли он не принадлежит
86
+ if (isHost) {
87
+ this._hiddenOutcome = null;
88
+ }
89
+
83
90
  // роль получена скрытой вкладкой: скрытие отсчитывается от получения
84
91
  if (isHost && this._hiddenSince !== null) {
85
92
  this._hiddenSince = this._hostSince;
@@ -98,6 +105,7 @@ export default class HostHealthPolicy {
98
105
  setHidden(hidden) {
99
106
  if (hidden && this._hiddenSince === null) {
100
107
  this._hiddenSince = this._now();
108
+ this._hiddenOutcome = null;
101
109
  this._armHiddenTimer();
102
110
  } else if (!hidden) {
103
111
  this._hiddenSince = null;
@@ -105,6 +113,19 @@ export default class HostHealthPolicy {
105
113
  }
106
114
  }
107
115
 
116
+ /**
117
+ * Исход передачи для последнего эпизода скрытия (живёт до следующего
118
+ * скрытия или новой роли хоста, переживает возврат видимости и потерю
119
+ * роли — его читает журнал закрываемого эпизода): null — порог
120
+ * hiddenHandoffMs не достигнут; 'started' — передача началась;
121
+ * 'disabled' | 'no-successor' | 'blocked' (срок роли или кулдаун) —
122
+ * ожидаемый отказ; 'unexplained' — всё разрешено, а передача не началась.
123
+ * @returns {string|null}
124
+ */
125
+ hiddenHandoff() {
126
+ return this._hiddenOutcome;
127
+ }
128
+
108
129
  // сэмпл здоровья Worker'а
109
130
  addHealth(health) {
110
131
  if (this._hostSince === null || !health) {
@@ -182,6 +203,8 @@ export default class HostHealthPolicy {
182
203
  }
183
204
 
184
205
  if (this._handOverNow('hidden')) {
206
+ this._hiddenOutcome = 'started';
207
+
185
208
  return true;
186
209
  }
187
210
 
@@ -191,9 +214,27 @@ export default class HostHealthPolicy {
191
214
  this._armHiddenTimer(waitMs);
192
215
  }
193
216
 
217
+ // начатая передача исход не меняет: следующие проверки её уже не стартуют
218
+ if (this._hiddenOutcome !== 'started') {
219
+ this._hiddenOutcome = this._refusal(waitMs);
220
+ }
221
+
194
222
  return false;
195
223
  }
196
224
 
225
+ // почему передача со скрытой вкладки не началась
226
+ _refusal(waitMs) {
227
+ if (!this._enabled()) {
228
+ return 'disabled';
229
+ }
230
+
231
+ if (!this._successor) {
232
+ return 'no-successor';
233
+ }
234
+
235
+ return waitMs > 0 ? 'blocked' : 'unexplained';
236
+ }
237
+
197
238
  // немедленная передача: ждущую границы — ускорить, иначе начать
198
239
  _handOverNow(reason) {
199
240
  if (!this._enabled() || !this._successor) {
@@ -11,7 +11,7 @@ import { createGuestIdentity } from '../../host/identity.js';
11
11
  // Почему inline, а не Worker: HostPlugin принципиально непередаваем в Worker
12
12
  // (postMessage не несёт функции), а standalone SDK получает от репозитория
13
13
  // игры именно живой объект плагина. Прод-путь (хост в Worker'е) не меняется —
14
- // расхождение dev/prod осознанное, см. plan/standalone-sdk/README.md.
14
+ // расхождение dev/prod осознанное.
15
15
  //
16
16
  // Мастера в этом контуре нет: личность гостевая (ник из формы, identity.js),
17
17
  // профиль offline-заглушкой (lib/offlinePlayerData.js).
@@ -102,6 +102,13 @@ export default class InlineHostBridge {
102
102
 
103
103
  this._runtime = runtime;
104
104
 
105
+ // сбой чат-команды игры виден в консоли вкладки вместе с кадрами обработчика
106
+ runtime.host.setDiagnosticSink(report =>
107
+ console.error(
108
+ `[vimp] ${report.message}${report.stack ? `\n${report.stack}` : ''}`,
109
+ ),
110
+ );
111
+
105
112
  this._portMachine = new PortMachine({
106
113
  host: runtime.host,
107
114
  socketManager: runtime.socketManager,
@@ -5,7 +5,7 @@ import Publisher from '../../lib/Publisher.js';
5
5
  // publisher с 'message'/'close', connect/send/close, — поэтому клиентский код
6
6
  // про него не знает.
7
7
  //
8
- // Отличия от WebRTC, которые надо держать в голове (см. docs/en/network.md):
8
+ // Отличия от WebRTC, которые надо держать в голове:
9
9
  // разделения meta/state тут нет, флаг reliable игнорируется. Значит PING/PONG
10
10
  // идут надёжным каналом (замер RTT становится замером TCP-пути), а бэкпрешер
11
11
  // вместо дропа позиционных кадров живёт на серверной стороне.
@@ -261,6 +261,11 @@ export default class HandoffFlow {
261
261
  this._policy?.addHealth(health);
262
262
  }
263
263
 
264
+ // исход передачи для последнего эпизода скрытия (HostHealthPolicy)
265
+ hiddenHandoff() {
266
+ return this._policy?.hiddenHandoff() ?? null;
267
+ }
268
+
264
269
  // «Leave server» из меню комнаты (этап 8.3). Хост при других людях отдаёт
265
270
  // роль бете без отката и уходит; один в комнате — закрывает её
266
271
  // (host_leaving: людей нет — мастер закрывает комнату сразу). Гость
@@ -321,11 +321,14 @@ export default class HostRole {
321
321
  onStats: this._debug ? stats => this._debug('standby sent', stats) : null,
322
322
  });
323
323
 
324
- // метрики здоровья Worker'а (этап 9a): скрытая вкладка троттлит цикл —
325
- // насколько, уходит в журнал одной сводкой за эпизод
324
+ // метрики здоровья Worker'а: эпизод скрытия, в котором матч просел,
325
+ // задел удалённых игроков, а автопередача не началась без видимой
326
+ // причины, уходит в журнал одной сводкой (HiddenHostHealthLog)
326
327
  this._hiddenLog = this._factories.hiddenLog({
327
328
  warn: (code, summary) =>
328
329
  this._diagnostics.warn(code, summary, { source: 'client' }),
330
+ minHealthyTickRate: migration.auto.overloadTickRate,
331
+ handoffOutcome: () => this._getHandoff().hiddenHandoff(),
329
332
  hidden: this._ui.isHidden(),
330
333
  });
331
334
  this._healthReporter = this._isLobbyMode
@@ -1,10 +1,9 @@
1
1
  @charset "UTF-8";
2
2
 
3
- /* База страницы: раньше жила инлайном в packages/engine/index.html, но в
4
- репозитории игры этого файла нет — а без правил ниже интерфейс движка
5
- остался бы без своей типографики и раскладки. Standalone SDK импортирует
6
- этот файл целиком, поэтому всё, что здесь есть, применяется и к
7
- встраивающей странице. */
3
+ /* База страницы: типографика и раскладка интерфейса движка живут здесь, а
4
+ не в index.html, потому что в репозитории игры такого файла нет.
5
+ Standalone SDK импортирует этот файл целиком, поэтому всё, что здесь
6
+ есть, применяется и к встраивающей странице. */
8
7
  html,
9
8
  body {
10
9
  width: 100%;
@@ -1329,7 +1328,7 @@ body.hide-cursor {
1329
1328
  color: #d66;
1330
1329
  }
1331
1330
 
1332
- /* журнал клиентских ошибок (plan/client-reports, этап 5): тот же каркас и
1331
+ /* журнал клиентских ошибок: тот же каркас и
1333
1332
  палитра, что у панели реестра игр; графы — .games-filters/.games-filter-btn */
1334
1333
  #reports-panel {
1335
1334
  background-color: #222;
@@ -55,9 +55,9 @@ const SHELL = [
55
55
  tag: 'div',
56
56
  id: 'auth-link',
57
57
  children: [
58
- // ссылка и версия пакета ИГРЫ — заполняет main.js по AUTH_DATA
59
- // из метаданных пакета в манифесте
60
- { tag: 'p', children: [{ tag: 'a', id: 'auth-package-link' }] },
58
+ // версия пакета ИГРЫ (из манифеста) и ссылка на документацию
59
+ // (только в dev-сборке) — заполняет main.js по AUTH_DATA
60
+ { tag: 'p', children: [{ tag: 'a', id: 'auth-docs-link' }] },
61
61
  { tag: 'p', id: 'auth-version' },
62
62
  { tag: 'p', text: `© ${new Date().getFullYear()} VIMP` },
63
63
  ],
@@ -15,10 +15,10 @@ div#auth
15
15
  div#auth-informs
16
16
 
17
17
  div#auth-link
18
- //- ссылка и версия пакета ИГРЫ (мастер кладёт их в манифест, читая
19
- //- package.json пакета) — проставляет main.js по AUTH_DATA; без данных
20
- //- ячейки остаются пустыми
18
+ //- версия пакета ИГРЫ (мастер кладёт её в манифест, читая package.json
19
+ //- пакета) и ссылка на документацию (только в dev-сборке) —
20
+ //- проставляет main.js по AUTH_DATA; без данных ячейки остаются пустыми
21
21
  p
22
- a#auth-package-link
22
+ a#auth-docs-link
23
23
  p#auth-version
24
24
  p &copy; #{new Date().getFullYear()} VIMP
@@ -21,7 +21,7 @@ div#lobby(style='display:none')
21
21
  input#profile-open(type='button', value='Profile')
22
22
  input#games-open-mine(type='button', value='My games')
23
23
  input#games-open-moderation(type='button', value='Moderation', style='display:none')
24
- //- журнал клиентских ошибок (plan/client-reports, этап 5): только админ
24
+ //- журнал клиентских ошибок: только админ
25
25
  input#reports-open(type='button', value='Errors', style='display:none')
26
26
  hr.lobby-divider
27
27
  h3.lobby-section-title= 'HOST A GAME'
@@ -32,7 +32,8 @@ div#lobby(style='display:none')
32
32
  label.form-label(for='lobby-game')= 'Select game'
33
33
  select#lobby-game.field-select
34
34
  //- поля настроек комнаты генерируются по явной схеме roomForm
35
- //- манифеста активной игры (docs/en/plugin-api.md "Form schema")
35
+ //- манифеста активной игры (docs.vimp.lgick.dev/en/plugin-api "Form
36
+ //- schema")
36
37
  div#lobby-fields
37
38
  //- ошибки формы (formBuilder.renderFormErrors) и отказ загрузки
38
39
  //- плагина выбранной игры — строкой в форме, а не в #tech-informer:
@@ -66,8 +67,8 @@ div#lobby(style='display:none')
66
67
  span#leaderboard-total
67
68
  ol#lobby-leaderboard-list
68
69
  p#lobby-my-placement
69
- //- футер: ссылку и версию пакета движка проставляет LobbyView
70
- //- (engineVersion.js + lib/packageLink.js)
70
+ //- футер: ссылка на документацию статична, версию пакета движка
71
+ //- проставляет LobbyView (engineVersion.js)
71
72
  div#lobby-footer
72
73
  p
73
74
  a#lobby-terms-link(target='_blank', rel='noopener') Terms of Service
@@ -75,6 +76,6 @@ div#lobby(style='display:none')
75
76
  |
76
77
  a#lobby-privacy-link(target='_blank', rel='noopener') Privacy Policy
77
78
  p
78
- a#lobby-link
79
+ a#lobby-link(href='https://docs.vimp.lgick.dev', target='_blank', rel='noopener') docs
79
80
  p#lobby-version
80
81
  p &copy; #{new Date().getFullYear()} VIMP
@@ -1,4 +1,4 @@
1
- //- Журнал клиентских ошибок (plan/client-reports, этап 5): только админ.
1
+ //- Журнал клиентских ошибок: только админ.
2
2
  //- Панель скрыта по умолчанию — открывается кнопкой «Errors» бейджа
3
3
  //- пользователя (#lobby-user в lobby.pug); id элементов перечислены в
4
4
  //- config/lobby.js. Подписи — по-английски, как и остальное лобби
@@ -1,10 +1,9 @@
1
1
  // Конфиг клиента профиля лобби. Бандлится в клиентскую сборку, как
2
2
  // config/lobby.js — auth-сервис живёт на отдельном домене/порте, поэтому
3
- // serviceUrl подставляется под конкретный деплой при сборке (см.
4
- // docs/en/auth.md).
3
+ // serviceUrl подставляется под конкретный деплой при сборке.
5
4
  export default {
6
- // базовый URL центрального auth-сервиса (packages/auth). Дефолт — dev-порт
7
- // из packages/auth/src/config/auth.js; в проде подставляется Vite'ом из
5
+ // базовый URL центрального auth-сервиса. Дефолт — dev-порт
6
+ // из конфига auth-сервиса; в проде подставляется Vite'ом из
8
7
  // VITE_AUTH_SERVICE_URL (см. Dockerfile — тот же VIMP_AUTH_SERVICE_URL,
9
8
  // что и серверный master/main.js). Этот модуль импортируется и серверным
10
9
  // master/main.js под чистым Node, где import.meta.env не определён —
@@ -15,7 +14,7 @@ export default {
15
14
  'http://localhost:3010',
16
15
 
17
16
  // issuer, которым central auth-сервис подписывает identity-токен; должен
18
- // совпадать с packages/auth/src/config/auth.js: jwt.issuer — хост сверяет
17
+ // совпадать с jwt.issuer из конфига auth-сервиса — хост сверяет
19
18
  // claim iss токена с этим значением при проверке подписи по /jwks
20
19
  issuer: 'vimp-auth',
21
20
 
@@ -1,6 +1,6 @@
1
1
  // коды игровых информ-сообщений (порт GAME_INFORM_DATA, см.
2
- // docs/en/network.md) — общий контракт хоста (SocketManager) и клиента
3
- // (main.js): значение не должно расходиться между ними молча
2
+ // docs.vimp.lgick.dev/en/snapshot) — общий контракт хоста (SocketManager) и
3
+ // клиента (main.js): значение не должно расходиться между ними молча
4
4
  export default {
5
5
  winnerTeam: [0],
6
6
  roundStart: [1],
@@ -300,11 +300,11 @@ export default {
300
300
  joinRetryWindowMs: 45000,
301
301
  },
302
302
 
303
- // приёмник выгрузок отладочного контура (этап 6 плана plan/done/ai-debug):
303
+ // приёмник выгрузок отладочного контура:
304
304
  // маршрут поднимается мастером только в dev, в проде вернёт 404
305
305
  debugReportUrl: '/debug/report',
306
306
 
307
- // журнал клиентских ошибок (plan/client-reports): приём на том же боксе,
307
+ // журнал клиентских ошибок: приём на том же боксе,
308
308
  // что раздал страницу, — лобби-мастер или dedicated
309
309
  clientReportUrl: '/client-reports',
310
310
 
@@ -350,9 +350,8 @@ export default {
350
350
  myPlacementId: 'lobby-my-placement',
351
351
 
352
352
  // футер: версия npm-пакета движка, запечённая в бандл при сборке
353
- // (client/lib/engineVersion.js), и ссылка на его страницу
353
+ // (client/lib/engineVersion.js)
354
354
  versionId: 'lobby-version',
355
- linkId: 'lobby-link',
356
355
  },
357
356
 
358
357
  // реестр игр (master-game-registry, этап 4): заявка разработчика и панель
@@ -489,7 +488,7 @@ export default {
489
488
  },
490
489
  },
491
490
 
492
- // журнал клиентских ошибок (plan/client-reports, этап 5): только админ.
491
+ // журнал клиентских ошибок: только админ.
493
492
  // URL и id элементов — здесь, как у панели реестра игр (reports.pug)
494
493
  clientReports: {
495
494
  urls: {
@@ -1,6 +1,7 @@
1
1
  // Разметка бинарного snapshot-протокола (порт SHOT_DATA).
2
2
  // Единый источник для хоста (упаковка) и клиента (распаковка) — обе стороны
3
- // живут в Rust-ядре: packages/engine/core/src/snapshot.rs (pack) и packages/engine/core/src/client/unpack.rs.
3
+ // живут в Rust-ядре: vimp-engine-core/src/snapshot.rs (pack) и
4
+ // vimp-engine-core/src/client/unpack.rs.
4
5
 
5
6
  // версия контракта движок ↔ игра-плагин (GameManifest, HostPlugin,
6
7
  // ClientPlugin, Wasm Host ABI — docs/{en,ru}/plugin-api.md).
@@ -1,5 +1,5 @@
1
- // Номера портов JSON-канала (docs/en/network.md). Реестр append-only, как и
2
- // остальные словари плагинной поверхности (этап 3 плана
1
+ // Номера портов JSON-канала (docs.vimp.lgick.dev/en/snapshot). Реестр
2
+ // append-only, как и остальные словари плагинной поверхности (этап 3 плана
3
3
  // plugin-forward-compat): номер НИКОГДА не переиспользуется и не
4
4
  // перенумеровывается — уже опубликованная сборка игры шлёт и слушает те
5
5
  // номера, что видела при сборке, и её dist больше никто не тронет.