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.
- package/LICENSE +21 -0
- package/README.md +12 -6
- package/bin/vimp-sim.js +2 -2
- package/core/Cargo.toml +4 -3
- package/package.json +14 -5
- package/src/client/boot.js +30 -7
- package/src/client/components/controller/ClientReports.js +1 -1
- package/src/client/components/controller/Games.js +1 -1
- package/src/client/components/model/ClientReports.js +1 -1
- package/src/client/components/model/Games.js +24 -0
- package/src/client/components/view/Auth.js +4 -2
- package/src/client/components/view/ClientReports.js +1 -1
- package/src/client/components/view/Games.js +19 -1
- package/src/client/components/view/Lobby.js +1 -7
- package/src/client/debug.js +1 -1
- package/src/client/lib/authApi.js +1 -1
- package/src/client/lib/diagnostics.js +19 -4
- package/src/client/lib/engineVersion.js +0 -6
- package/src/client/lib/footerLink.js +15 -14
- package/src/client/lib/formBuilder.js +9 -8
- package/src/client/lib/retryOnNetworkError.js +60 -0
- package/src/client/main.js +46 -28
- package/src/client/network/HiddenHostHealthLog.js +40 -3
- package/src/client/network/HostController.js +4 -4
- package/src/client/network/HostHealthPolicy.js +41 -0
- package/src/client/network/InlineHostBridge.js +8 -1
- package/src/client/network/WebSocketTransport.js +1 -1
- package/src/client/session/HandoffFlow.js +5 -0
- package/src/client/session/HostRole.js +5 -2
- package/src/client/style.css +5 -6
- package/src/client/views/gameShell.js +3 -3
- package/src/client/views/includes/auth.pug +4 -4
- package/src/client/views/includes/lobby.pug +6 -5
- package/src/client/views/includes/reports.pug +1 -1
- package/src/config/authClient.js +4 -5
- package/src/config/gameCodes.js +2 -2
- package/src/config/lobby.js +4 -5
- package/src/config/opcodes.js +2 -1
- package/src/config/wsports.js +2 -2
- package/src/devtools/builtinScenario.js +3 -3
- package/src/devtools/contract/loadContext.js +2 -2
- package/src/devtools/contract/rules/a2-package-scripts.js +4 -3
- package/src/devtools/contract/rules/a4-vite-config.js +4 -3
- package/src/devtools/contract/rules/a7-package-repository.js +7 -7
- package/src/devtools/contract/rules/c4-component-dependencies.js +5 -5
- package/src/devtools/contract/rules/index.js +1 -1
- package/src/devtools/invariants.js +2 -2
- package/src/devtools/pluginLoader.js +3 -3
- package/src/host/DebugRecorder.js +1 -1
- package/src/host/HostGame.js +35 -15
- package/src/host/PortMachine.js +6 -5
- package/src/host/commandFailure.js +140 -0
- package/src/host/host.worker.js +8 -2
- package/src/host/meta/SocketManager.js +2 -2
- package/src/host/meta/core/CommandProcessor.js +37 -7
- package/src/lib/applyRoomOverrides.js +1 -1
- package/src/lib/capabilities.js +1 -1
- package/src/lib/checkpointCodec.js +1 -2
- package/src/lib/coreConfig.js +1 -1
- package/src/lib/createHostRuntime.js +1 -1
- package/src/lib/gameId.js +1 -1
- package/src/lib/gamePlugin.js +6 -2
- package/src/lib/jwt.js +1 -1
- package/src/lib/loadGamePackage.js +1 -1
- package/src/lib/packageLink.js +21 -63
- package/src/lib/validators.js +3 -3
- package/src/standalone/index.js +6 -6
- package/tests/fixtures/generations/gen-api3/README.md +2 -2
- package/tests/fixtures/generations/gen-api4/README.md +2 -2
- package/bin/vimp-surface.js +0 -88
- package/src/config/env.js +0 -104
- package/src/config/master.js +0 -288
- package/src/devtools/surface/abiParse.js +0 -175
- package/src/devtools/surface/collect.js +0 -413
- package/src/lib/clientIp.js +0 -96
- package/src/lib/rateLimiter.js +0 -40
- package/src/lib/security.js +0 -45
package/src/client/main.js
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
bindLiveErrors,
|
|
33
33
|
} from './lib/formBuilder.js';
|
|
34
34
|
import { normalizeAuthParams } from './lib/authParams.js';
|
|
35
|
-
import {
|
|
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)
|
|
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
|
-
// журнал клиентских
|
|
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 подставляет константу): включает отладочный контур
|
|
352
|
-
//
|
|
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
|
-
// журнал клиентских ошибок для партов
|
|
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
|
-
// футер формы входа: версия
|
|
581
|
-
//
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
//
|
|
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
|
-
|
|
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, поэтому панель им не управляет
|
|
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
|
-
// иначе его события в браузере не видны
|
|
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
|
|
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
|
-
// журнал клиентских
|
|
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-форме) туда не сериализуются
|
|
2573
|
-
// "Form schema"); авторитетный клампинг
|
|
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
|
-
//
|
|
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({
|
|
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
|
|
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
|
|
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
|
-
// журнал клиентских
|
|
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
|
-
* Отладочный
|
|
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
|
|
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, которые надо держать в
|
|
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'
|
|
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
|
package/src/client/style.css
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
@charset "UTF-8";
|
|
2
2
|
|
|
3
|
-
/* База страницы:
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
/* журнал клиентских
|
|
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
|
-
//
|
|
59
|
-
//
|
|
60
|
-
{ tag: 'p', children: [{ tag: 'a', id: 'auth-
|
|
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
|
-
//-
|
|
20
|
-
//- ячейки остаются пустыми
|
|
18
|
+
//- версия пакета ИГРЫ (мастер кладёт её в манифест, читая package.json
|
|
19
|
+
//- пакета) и ссылка на документацию (только в dev-сборке) —
|
|
20
|
+
//- проставляет main.js по AUTH_DATA; без данных ячейки остаются пустыми
|
|
21
21
|
p
|
|
22
|
-
a#auth-
|
|
22
|
+
a#auth-docs-link
|
|
23
23
|
p#auth-version
|
|
24
24
|
p © #{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
|
-
//- журнал клиентских
|
|
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
|
|
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
|
-
//- футер:
|
|
70
|
-
//- (engineVersion.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 © #{new Date().getFullYear()} VIMP
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//- Журнал клиентских
|
|
1
|
+
//- Журнал клиентских ошибок: только админ.
|
|
2
2
|
//- Панель скрыта по умолчанию — открывается кнопкой «Errors» бейджа
|
|
3
3
|
//- пользователя (#lobby-user в lobby.pug); id элементов перечислены в
|
|
4
4
|
//- config/lobby.js. Подписи — по-английски, как и остальное лобби
|
package/src/config/authClient.js
CHANGED
|
@@ -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
|
|
7
|
-
// из
|
|
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
|
-
// совпадать с
|
|
17
|
+
// совпадать с jwt.issuer из конфига auth-сервиса — хост сверяет
|
|
19
18
|
// claim iss токена с этим значением при проверке подписи по /jwks
|
|
20
19
|
issuer: 'vimp-auth',
|
|
21
20
|
|
package/src/config/gameCodes.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// коды игровых информ-сообщений (порт GAME_INFORM_DATA, см.
|
|
2
|
-
// docs/en/
|
|
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],
|
package/src/config/lobby.js
CHANGED
|
@@ -300,11 +300,11 @@ export default {
|
|
|
300
300
|
joinRetryWindowMs: 45000,
|
|
301
301
|
},
|
|
302
302
|
|
|
303
|
-
// приёмник выгрузок отладочного
|
|
303
|
+
// приёмник выгрузок отладочного контура:
|
|
304
304
|
// маршрут поднимается мастером только в dev, в проде вернёт 404
|
|
305
305
|
debugReportUrl: '/debug/report',
|
|
306
306
|
|
|
307
|
-
// журнал клиентских
|
|
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
|
-
// журнал клиентских
|
|
491
|
+
// журнал клиентских ошибок: только админ.
|
|
493
492
|
// URL и id элементов — здесь, как у панели реестра игр (reports.pug)
|
|
494
493
|
clientReports: {
|
|
495
494
|
urls: {
|
package/src/config/opcodes.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Разметка бинарного snapshot-протокола (порт SHOT_DATA).
|
|
2
2
|
// Единый источник для хоста (упаковка) и клиента (распаковка) — обе стороны
|
|
3
|
-
// живут в Rust-ядре:
|
|
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).
|
package/src/config/wsports.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Номера портов JSON-канала (docs/en/
|
|
2
|
-
// остальные словари плагинной поверхности (этап 3 плана
|
|
1
|
+
// Номера портов JSON-канала (docs.vimp.lgick.dev/en/snapshot). Реестр
|
|
2
|
+
// append-only, как и остальные словари плагинной поверхности (этап 3 плана
|
|
3
3
|
// plugin-forward-compat): номер НИКОГДА не переиспользуется и не
|
|
4
4
|
// перенумеровывается — уже опубликованная сборка игры шлёт и слушает те
|
|
5
5
|
// номера, что видела при сборке, и её dist больше никто не тронет.
|