vimp-engine 0.35.5 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/vimp-sim.js +19 -1
- package/core/Cargo.toml +1 -1
- package/package.json +1 -1
- package/src/client/components/controller/Controls.js +5 -0
- package/src/client/components/controller/Lobby.js +4 -4
- package/src/client/components/controller/RoomMenu.js +43 -0
- package/src/client/components/controller/Vote.js +18 -2
- package/src/client/components/model/ClientReports.js +6 -2
- package/src/client/components/model/Controls.js +27 -0
- package/src/client/components/model/Games.js +5 -1
- package/src/client/components/model/Lobby.js +32 -24
- package/src/client/components/model/LobbyAuth.js +20 -4
- package/src/client/components/model/RoomMenu.js +121 -0
- package/src/client/components/model/Stat.js +2 -1
- package/src/client/components/model/Vote.js +122 -4
- package/src/client/components/view/Auth.js +20 -10
- package/src/client/components/view/ClientReports.js +16 -5
- package/src/client/components/view/Games.js +39 -14
- package/src/client/components/view/Lobby.js +90 -43
- package/src/client/components/view/LobbyAuth.js +13 -7
- package/src/client/components/view/Panel.js +1 -2
- package/src/client/components/view/RoomMenu.js +69 -0
- package/src/client/debug.js +24 -1
- package/src/client/lib/FpsMeter.js +46 -0
- package/src/client/lib/JoinRetry.js +73 -0
- package/src/client/lib/diagnostics.js +28 -5
- package/src/client/lib/hostCaps.js +82 -0
- package/src/client/lib/hostUnloadGuard.js +108 -0
- package/src/client/lib/hostVoteCommand.js +92 -0
- package/src/client/lib/pageReload.js +29 -0
- package/src/client/lib/roomLink.js +257 -0
- package/src/client/lib/signalingErrors.js +74 -0
- package/src/client/main.js +674 -451
- package/src/client/network/HiddenHostHealthLog.js +76 -0
- package/src/client/network/HostConnectionManager.js +62 -8
- package/src/client/network/HostController.js +376 -23
- package/src/client/network/HostHealthPolicy.js +307 -0
- package/src/client/network/HostHealthReporter.js +54 -0
- package/src/client/network/HostPrewarm.js +235 -0
- package/src/client/network/LoopbackTransport.js +16 -1
- package/src/client/network/PlannedHandoff.js +366 -0
- package/src/client/network/Promotion.js +420 -0
- package/src/client/network/RoomPeersReporter.js +81 -0
- package/src/client/network/SessionSupervisor.js +730 -0
- package/src/client/network/SignalingClient.js +183 -30
- package/src/client/network/StandbyReceiver.js +287 -0
- package/src/client/network/StandbySender.js +319 -0
- package/src/client/network/TokenHandoffTimer.js +81 -0
- package/src/client/network/WebRtcManager.js +338 -11
- package/src/client/network/policyClose.js +32 -2
- package/src/client/network/standbyChunks.js +185 -0
- package/src/client/session/GuestSession.js +428 -0
- package/src/client/session/HandoffFlow.js +399 -0
- package/src/client/session/HostRole.js +708 -0
- package/src/client/session/Membership.js +179 -0
- package/src/client/session/PromotionFlow.js +325 -0
- package/src/client/session/RouteBoot.js +432 -0
- package/src/client/session/StandbyRole.js +195 -0
- package/src/client/session/hostRoomPrep.js +152 -0
- package/src/client/session/roomContext.js +32 -0
- package/src/client/style.css +83 -22
- package/src/client/views/gameShell.js +7 -0
- package/src/client/views/includes/informer.pug +4 -0
- package/src/client/views/includes/lobby.pug +0 -3
- package/src/client/views/includes/roomMenu.pug +9 -0
- package/src/client/views/index.pug +1 -0
- package/src/config/closeCodes.js +1 -1
- package/src/config/env.js +37 -0
- package/src/config/hostDefaults.js +24 -0
- package/src/config/lobby.js +154 -3
- package/src/config/master.js +104 -21
- package/src/config/wsports.js +8 -0
- package/src/devtools/RecordingSocketManager.js +3 -2
- package/src/devtools/ScenarioRunner.js +123 -8
- package/src/devtools/VirtualClient.js +20 -0
- package/src/devtools/contract/format.js +4 -1
- package/src/devtools/contract/index.js +1 -2
- package/src/devtools/contract/rules/a2-package-scripts.js +1 -3
- package/src/devtools/contract/rules/b8-system-messages.js +1 -1
- package/src/devtools/contract/rules/b9-vote-names.js +25 -1
- package/src/devtools/contract/rules/c11-stat-mode.js +4 -1
- package/src/devtools/contract/rules/c6-stat-columns.js +1 -1
- package/src/devtools/contract/rules/c7-key-sets.js +8 -4
- package/src/devtools/contract/rules/d3-snapshot-interp.js +2 -1
- package/src/devtools/contract/rules/e1-sound-pairs.js +3 -1
- package/src/devtools/contract/rules/e4-map-layers.js +13 -2
- package/src/devtools/contract/rules/e5-map-radar-walls.js +7 -1
- package/src/devtools/contract/rules/index.js +38 -5
- package/src/devtools/invariants.js +10 -4
- package/src/devtools/pluginLoader.js +1 -4
- package/src/devtools/report.js +17 -1
- package/src/host/DebugRecorder.js +6 -1
- package/src/host/GameCoreAdapter.js +45 -0
- package/src/host/HostGame.js +928 -59
- package/src/host/PortMachine.js +268 -20
- package/src/host/host.worker.js +222 -16
- package/src/host/meta/SocketManager.js +20 -0
- package/src/host/meta/core/RoundManager.js +89 -14
- package/src/host/meta/modules/Accolades.js +4 -1
- package/src/host/meta/modules/Panel.js +30 -0
- package/src/host/meta/modules/PlayerDataSync.js +255 -46
- package/src/host/meta/modules/RTTManager.js +34 -0
- package/src/host/meta/modules/TimerManager.js +215 -18
- package/src/host/meta/modules/chat/systemMessages.js +22 -0
- package/src/host/meta/player/HumanParticipant.js +10 -0
- package/src/host/meta/player/ParticipantManager.js +98 -5
- package/src/lib/AbstractTimer.js +63 -1
- package/src/lib/Publisher.js +26 -0
- package/src/lib/capabilities.js +5 -0
- package/src/lib/checkpointCodec.js +125 -0
- package/src/lib/clientIp.js +9 -2
- package/src/lib/createHostRuntime.js +95 -6
- package/src/lib/formOptions.js +3 -1
- package/src/lib/gameConfigView.js +10 -0
- package/src/lib/gameId.js +13 -0
- package/src/lib/jwt.js +3 -1
- package/src/lib/loadGamePackage.js +2 -2
- package/src/lib/resumeKey.js +38 -0
- package/src/lib/roomId.js +39 -0
- package/src/lib/roomSettings.js +59 -0
- package/src/lib/validators.js +7 -0
- package/tests/fixtures/generations/gen-api3/host/ScriptedManager.js +2 -3
- package/tests/fixtures/generations/gen-api3/host/fakeCore.js +31 -5
- package/tests/fixtures/generations/gen-api3/manifest.json +1 -3
- package/tests/fixtures/generations/gen-api4/host/ScriptedManager.js +2 -3
- package/tests/fixtures/generations/gen-api4/host/fakeCore.js +31 -5
- package/tests/fixtures/generations/gen-api4/manifest.json +1 -3
- package/tests/fixtures/miniGame/config/client.js +15 -0
- package/tests/fixtures/miniGame/host/ScriptedManager.js +2 -3
- package/tests/fixtures/miniGame/host/fakeCore.js +31 -5
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Подготовка комнаты к Worker'у хоста: room.game по манифесту игры, карты
|
|
2
|
+
// мастера, URL worker-бандла. Общая для создания комнаты, прогрева
|
|
3
|
+
// преемника (host-migration этап 6, HostPrewarm) и эстафеты Worker'ов.
|
|
4
|
+
//
|
|
5
|
+
// Без DOM: активную игру, конфиг и сеть инъектирует сборщик
|
|
6
|
+
// (client/main.js).
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {Object} deps
|
|
10
|
+
* @param {Function} deps.getActiveGame - () → манифест активной игры.
|
|
11
|
+
* @param {Object} deps.config - lobby-конфиг (maps, worker, game).
|
|
12
|
+
* @param {boolean} deps.isDevBuild
|
|
13
|
+
* @param {Function} deps.fetchGamePluginManifest - lib/gamePlugin.js.
|
|
14
|
+
* @param {Function} [deps.fetch]
|
|
15
|
+
*/
|
|
16
|
+
export function createHostRoomPrep({
|
|
17
|
+
getActiveGame,
|
|
18
|
+
config,
|
|
19
|
+
isDevBuild,
|
|
20
|
+
fetchGamePluginManifest,
|
|
21
|
+
fetch = (...args) => globalThis.fetch(...args),
|
|
22
|
+
}) {
|
|
23
|
+
// Этап 5.1/6.4: скачивает каталог карт мастера активной игры (манифест +
|
|
24
|
+
// все карты)
|
|
25
|
+
async function fetchMasterMaps(gameManifest = getActiveGame()) {
|
|
26
|
+
const manifestRes = await fetch(config.maps.manifestUrl(gameManifest));
|
|
27
|
+
|
|
28
|
+
if (!manifestRes.ok) {
|
|
29
|
+
throw new Error(`maps manifest: HTTP ${manifestRes.status}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const manifest = await manifestRes.json();
|
|
33
|
+
|
|
34
|
+
const entries = await Promise.all(
|
|
35
|
+
manifest.maps.map(async name => {
|
|
36
|
+
const url = `${config.maps.baseUrl(gameManifest)}/${encodeURIComponent(name)}`;
|
|
37
|
+
const res = await fetch(url);
|
|
38
|
+
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
throw new Error(`map ${name}: HTTP ${res.status}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return [name, await res.json()];
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return { version: manifest.version, maps: Object.fromEntries(entries) };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Этап 5.2: скачивает манифест worker-бандла мастера ({ version, url })
|
|
51
|
+
async function fetchWorkerManifest() {
|
|
52
|
+
const res = await fetch(config.worker.manifestUrl);
|
|
53
|
+
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
throw new Error(`worker manifest: HTTP ${res.status}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return res.json();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Этап 6.5: перечитывает манифест активной игры мастера — своп не должен
|
|
62
|
+
// нести новому Worker'у закэшированный с момента создания комнаты
|
|
63
|
+
// hostEntryUrl/wasmUrl (деплой игры мог обновиться независимо от движка)
|
|
64
|
+
async function fetchGameManifest(gameId) {
|
|
65
|
+
return fetchGamePluginManifest(config.game.manifestUrl(gameId));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// манифест игры по ссылке на неё: уже загруженный активный — как есть,
|
|
69
|
+
// другая версия — версионный манифест мастера
|
|
70
|
+
async function resolveGameManifest(gameRef) {
|
|
71
|
+
if (gameRef?.entries) {
|
|
72
|
+
return gameRef;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const active = getActiveGame();
|
|
76
|
+
|
|
77
|
+
if (
|
|
78
|
+
active &&
|
|
79
|
+
gameRef.id === active.id &&
|
|
80
|
+
gameRef.version === active.version
|
|
81
|
+
) {
|
|
82
|
+
return active;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return fetchGamePluginManifest(
|
|
86
|
+
config.game.versionManifestUrl(gameRef.id, gameRef.version),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// gameRef — манифест игры или { id, version } из контрольной точки: бета
|
|
91
|
+
// обязана поднять ту версию игры, что крутится в комнате (версионные URL
|
|
92
|
+
// мастера)
|
|
93
|
+
async function prepareHostRoom(room, gameRef = getActiveGame()) {
|
|
94
|
+
const gameManifest = await resolveGameManifest(gameRef);
|
|
95
|
+
|
|
96
|
+
// отладочный контур (этап 6): рекордер живого матча и хостовый
|
|
97
|
+
// CONSOLE-лог поднимаются только в dev-сборке
|
|
98
|
+
room.isDevMode = isDevBuild;
|
|
99
|
+
|
|
100
|
+
// Этап 6.4: Worker грузит HostPlugin динамически по
|
|
101
|
+
// entries.host/entries.wasm активной игры — движок не знает игру
|
|
102
|
+
// статически
|
|
103
|
+
room.game = {
|
|
104
|
+
id: gameManifest.id,
|
|
105
|
+
version: gameManifest.version,
|
|
106
|
+
hostEntryUrl: gameManifest.entries.host,
|
|
107
|
+
wasmUrl: gameManifest.entries.wasm,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Этап 5.1: комната стартует на актуальных картах мастера;
|
|
111
|
+
// недоступность каталога некритична — Worker возьмёт карты из бандла
|
|
112
|
+
let mapsVersion = null;
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const catalog = await fetchMasterMaps(gameManifest);
|
|
116
|
+
|
|
117
|
+
room.maps = catalog.maps;
|
|
118
|
+
mapsVersion = catalog.version;
|
|
119
|
+
} catch (e) {
|
|
120
|
+
console.warn('[maps] master catalog unavailable, using bundled maps:', e);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Этап 5.2: Worker создаётся по манифесту мастера — бандл страницы после
|
|
124
|
+
// деплоя исчезает из раздачи; без манифеста (dev) — бандловый URL,
|
|
125
|
+
// обновления кода отключены
|
|
126
|
+
let workerUrl = null;
|
|
127
|
+
let codeVersion = null;
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const manifest = await fetchWorkerManifest();
|
|
131
|
+
|
|
132
|
+
// составной codeVersion (Этап 6.5): движок (worker-бандл) + игра
|
|
133
|
+
// (id/version манифеста, с которым комната стартует)
|
|
134
|
+
codeVersion = {
|
|
135
|
+
engine: manifest.version,
|
|
136
|
+
game: { id: gameManifest.id, version: gameManifest.version },
|
|
137
|
+
};
|
|
138
|
+
workerUrl = manifest.url;
|
|
139
|
+
} catch (e) {
|
|
140
|
+
console.warn('[worker] master manifest unavailable, using bundled:', e);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { room, workerUrl, mapsVersion, codeVersion };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
prepareHostRoom,
|
|
148
|
+
fetchMasterMaps,
|
|
149
|
+
fetchWorkerManifest,
|
|
150
|
+
fetchGameManifest,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Общее состояние вкладки в комнате (лобби-режим): его читают и меняют
|
|
2
|
+
// роли хоста, гостя, беты, промоушен и маршруты (client/session/*).
|
|
3
|
+
// Обычный объект, а не класс: модули получают одну и ту же ссылку от
|
|
4
|
+
// сборщика (client/main.js), тесты — свежую на каждый сценарий.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @returns {{
|
|
8
|
+
* roomId: string|null,
|
|
9
|
+
* epoch: number|null,
|
|
10
|
+
* memberJoined: boolean,
|
|
11
|
+
* entered: boolean,
|
|
12
|
+
* startFailed: boolean,
|
|
13
|
+
* }}
|
|
14
|
+
*/
|
|
15
|
+
export function createRoomContext() {
|
|
16
|
+
return {
|
|
17
|
+
// комната, в которой вкладка гость или хост
|
|
18
|
+
roomId: null,
|
|
19
|
+
// эпоха хоста комнаты, ответившего последним: ответы прежних эпох чужие
|
|
20
|
+
epoch: null,
|
|
21
|
+
// вкладка объявила мастеру членство (join_room) и повторяет его после
|
|
22
|
+
// реконнекта сигналинга — даже если WebRTC в этот момент переподключается
|
|
23
|
+
memberJoined: false,
|
|
24
|
+
// вкладка ушла из лобби в комнату (по ссылке или из лобби). Не
|
|
25
|
+
// сбрасывается: после разрыва страница — остатки матча, и новый маршрут
|
|
26
|
+
// из hashchange поднимается перезагрузкой, а не бутстрапом поверх них
|
|
27
|
+
entered: false,
|
|
28
|
+
// Worker комнаты не поднялся: та же быстрая игра создала бы её снова и
|
|
29
|
+
// снова — поэтому уход на главную, как при кике
|
|
30
|
+
startFailed: false,
|
|
31
|
+
};
|
|
32
|
+
}
|
package/src/client/style.css
CHANGED
|
@@ -86,7 +86,9 @@ body.hide-cursor {
|
|
|
86
86
|
left: 0;
|
|
87
87
|
right: 0;
|
|
88
88
|
height: 24px;
|
|
89
|
-
|
|
89
|
+
/* 7 — выше прозрачного #game-informer (6), иначе тот перехватывал бы
|
|
90
|
+
клики по кнопке меню комнаты; ниже лобби/экрана авторизации игры (8) */
|
|
91
|
+
z-index: 7;
|
|
90
92
|
overflow: hidden;
|
|
91
93
|
}
|
|
92
94
|
|
|
@@ -359,6 +361,58 @@ body.hide-cursor {
|
|
|
359
361
|
display: block;
|
|
360
362
|
}
|
|
361
363
|
|
|
364
|
+
/*room menu (host-migration, этап 3): кнопка внутри #panel справа от
|
|
365
|
+
таблицы (RoomMenuView.placeInPanel); order — таблицу PanelView достраивает
|
|
366
|
+
позже, по CONFIG_DATA. Без панели (игра её не показывает) — угол экрана;
|
|
367
|
+
z-index 7 — выше прозрачного #game-informer (6) и ниже лобби (8)*/
|
|
368
|
+
|
|
369
|
+
#room-menu {
|
|
370
|
+
position: absolute;
|
|
371
|
+
top: 0;
|
|
372
|
+
right: 0;
|
|
373
|
+
z-index: 7;
|
|
374
|
+
font-size: 12px;
|
|
375
|
+
text-transform: none;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
#panel > #room-menu {
|
|
379
|
+
position: static;
|
|
380
|
+
order: 1;
|
|
381
|
+
flex: 0 0 auto;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
#room-menu-toggle {
|
|
385
|
+
display: block;
|
|
386
|
+
margin-left: auto;
|
|
387
|
+
height: 24px;
|
|
388
|
+
padding: 0 8px;
|
|
389
|
+
border: none;
|
|
390
|
+
background: transparent;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/* fixed — иначе список обрезал бы overflow: hidden панели */
|
|
394
|
+
#room-menu-list {
|
|
395
|
+
position: fixed;
|
|
396
|
+
top: 24px;
|
|
397
|
+
right: 0;
|
|
398
|
+
padding: 4px;
|
|
399
|
+
background: #fff;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
#room-menu-list input {
|
|
403
|
+
display: block;
|
|
404
|
+
width: 100%;
|
|
405
|
+
white-space: nowrap;
|
|
406
|
+
margin-top: 5px;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
#room-menu-status {
|
|
410
|
+
padding: 2px 4px;
|
|
411
|
+
/* тёмный: фон списка белый */
|
|
412
|
+
color: #333;
|
|
413
|
+
white-space: nowrap;
|
|
414
|
+
}
|
|
415
|
+
|
|
362
416
|
/*chat*/
|
|
363
417
|
|
|
364
418
|
#chat {
|
|
@@ -633,6 +687,25 @@ body.hide-cursor {
|
|
|
633
687
|
white-space: pre-line;
|
|
634
688
|
}
|
|
635
689
|
|
|
690
|
+
/* оверлей сессии (host-migration этап 4): матч на паузе, пока клиент
|
|
691
|
+
переподключается к хосту; уровень лобби/авторизации — они с ним не
|
|
692
|
+
показываются одновременно, а #tech-informer остаётся выше. Своего
|
|
693
|
+
display нет: скрыт он `.vimp-shell > *`, показывает его main.js (flex) */
|
|
694
|
+
#session-overlay {
|
|
695
|
+
position: absolute;
|
|
696
|
+
top: 0;
|
|
697
|
+
left: 0;
|
|
698
|
+
z-index: 8;
|
|
699
|
+
align-items: center;
|
|
700
|
+
justify-content: center;
|
|
701
|
+
width: 100%;
|
|
702
|
+
height: 100%;
|
|
703
|
+
background-color: rgb(0 0 0 / 60%);
|
|
704
|
+
color: #fff;
|
|
705
|
+
font-size: 20px;
|
|
706
|
+
line-height: 24px;
|
|
707
|
+
}
|
|
708
|
+
|
|
636
709
|
#game-informer {
|
|
637
710
|
position: absolute;
|
|
638
711
|
top: 0;
|
|
@@ -700,13 +773,6 @@ body.hide-cursor {
|
|
|
700
773
|
gap: 10px;
|
|
701
774
|
}
|
|
702
775
|
|
|
703
|
-
/* поле имени сервера — строка формы, а не узкий числовой инпут
|
|
704
|
-
(.form-row input[type='text'] рассчитан на счётчики вроде max players) */
|
|
705
|
-
#lobby-name {
|
|
706
|
-
width: 200px;
|
|
707
|
-
text-align: left;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
776
|
#lobby input[type='text'],
|
|
711
777
|
#lobby select {
|
|
712
778
|
box-sizing: border-box;
|
|
@@ -741,7 +807,7 @@ body.hide-cursor {
|
|
|
741
807
|
|
|
742
808
|
.lobby-card {
|
|
743
809
|
display: grid;
|
|
744
|
-
grid-template-columns: 1fr auto
|
|
810
|
+
grid-template-columns: 1fr auto;
|
|
745
811
|
column-gap: 8px;
|
|
746
812
|
row-gap: 2px;
|
|
747
813
|
padding: 10px 12px;
|
|
@@ -758,13 +824,19 @@ body.hide-cursor {
|
|
|
758
824
|
}
|
|
759
825
|
|
|
760
826
|
.lobby-card-name {
|
|
761
|
-
grid-column: 1
|
|
827
|
+
grid-column: 1;
|
|
762
828
|
font-weight: bold;
|
|
763
829
|
overflow: hidden;
|
|
764
830
|
text-overflow: ellipsis;
|
|
765
831
|
white-space: nowrap;
|
|
766
832
|
}
|
|
767
833
|
|
|
834
|
+
.lobby-card-copy {
|
|
835
|
+
grid-column: 2;
|
|
836
|
+
font-size: 10px;
|
|
837
|
+
padding: 0 6px;
|
|
838
|
+
}
|
|
839
|
+
|
|
768
840
|
.lobby-card-info {
|
|
769
841
|
grid-column: 1;
|
|
770
842
|
color: #aaa;
|
|
@@ -774,19 +846,8 @@ body.hide-cursor {
|
|
|
774
846
|
white-space: nowrap;
|
|
775
847
|
}
|
|
776
848
|
|
|
777
|
-
.lobby-card-rating {
|
|
778
|
-
grid-column: 2;
|
|
779
|
-
text-align: right;
|
|
780
|
-
color: #4caf50;
|
|
781
|
-
font-size: 10px;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
.lobby-card-rating.is-negative {
|
|
785
|
-
color: #e57373;
|
|
786
|
-
}
|
|
787
|
-
|
|
788
849
|
.lobby-card-latency {
|
|
789
|
-
grid-column:
|
|
850
|
+
grid-column: 2;
|
|
790
851
|
text-align: right;
|
|
791
852
|
color: #ff0;
|
|
792
853
|
font-size: 10px;
|
|
@@ -67,6 +67,13 @@ const SHELL = [
|
|
|
67
67
|
],
|
|
68
68
|
},
|
|
69
69
|
{ tag: 'div', id: 'game-informer' },
|
|
70
|
+
// оверлей переподключения (host-migration этап 4): в solo/dedicated не
|
|
71
|
+
// показывается, но разметка общая с informer.pug
|
|
72
|
+
{
|
|
73
|
+
tag: 'div',
|
|
74
|
+
id: 'session-overlay',
|
|
75
|
+
children: [{ tag: 'span', id: 'session-overlay-text' }],
|
|
76
|
+
},
|
|
70
77
|
{ tag: 'div', id: 'tech-informer' },
|
|
71
78
|
];
|
|
72
79
|
|
|
@@ -31,9 +31,6 @@ div#lobby(style='display:none')
|
|
|
31
31
|
div.form-row
|
|
32
32
|
label.form-label(for='lobby-game')= 'Select game'
|
|
33
33
|
select#lobby-game.field-select
|
|
34
|
-
div.form-row
|
|
35
|
-
label.form-label(for='lobby-name')= 'Server name'
|
|
36
|
-
input#lobby-name.field-text(type='text', maxlength='30', autocomplete='off')
|
|
37
34
|
//- поля настроек комнаты генерируются по явной схеме roomForm
|
|
38
35
|
//- манифеста активной игры (docs/en/plugin-api.md "Form schema")
|
|
39
36
|
div#lobby-fields
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//- меню комнаты (host-migration, этапы 3 и 8d): только лобби-режим, пока
|
|
2
|
+
//- вкладка в комнате. «Hand over host» — только у хоста, когда передавать
|
|
3
|
+
//- есть кому; статус передачи — на месте заблокированных пунктов
|
|
4
|
+
div#room-menu(style='display:none')
|
|
5
|
+
input#room-menu-toggle(type='button', value='☰', title='Room menu')
|
|
6
|
+
div#room-menu-list(style='display:none')
|
|
7
|
+
input#room-menu-handover(type='button', value='Hand over host', style='display:none')
|
|
8
|
+
input#room-menu-leave(type='button', value='Leave server')
|
|
9
|
+
div#room-menu-status(style='display:none')
|
package/src/config/closeCodes.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
export default {
|
|
8
8
|
staleHost: 4000, // master/SignalingServer.js — сигнальный WS хоста протух
|
|
9
9
|
invalidOrigin: 4001, // master/SignalingServer.js, dedicated/main.js
|
|
10
|
-
|
|
10
|
+
// 4002 retired (server rating removed) — номер не переиспользуется
|
|
11
11
|
kickForMaxLatency: 4003, // host/HostGame.js
|
|
12
12
|
kickForMissedPings: 4004, // host/HostGame.js
|
|
13
13
|
kickIdle: 4005, // host/HostGame.js
|
package/src/config/env.js
CHANGED
|
@@ -39,6 +39,43 @@ export function applyMasterEnv(config, env = process.env) {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// минимальная длина VIMP_ROOM_SECRET_KEY — ключ HMAC-SHA256
|
|
43
|
+
const ROOM_SECRET_KEY_MIN_BYTES = 32;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Ключ секрета комнаты (host-migration, этап 2): roomSecret = HMAC(ключ,
|
|
47
|
+
* roomId:epoch:hostUserId) — им хост доказывает комнату после рестарта
|
|
48
|
+
* мастера. В production обязателен; в dev без него генерируется случайный на
|
|
49
|
+
* время процесса (комнаты после рестарта dev-мастера не восстанавливаются).
|
|
50
|
+
* @param {Object} [env] - Окружение (по умолчанию process.env).
|
|
51
|
+
* @param {Object} opts
|
|
52
|
+
* @param {boolean} opts.isProduction
|
|
53
|
+
* @param {Function} opts.randomBytes - crypto.randomBytes.
|
|
54
|
+
* @returns {{ key: string|Buffer, ephemeral: boolean }}
|
|
55
|
+
*/
|
|
56
|
+
export function readRoomSecretKey(
|
|
57
|
+
env = process.env,
|
|
58
|
+
{ isProduction, randomBytes },
|
|
59
|
+
) {
|
|
60
|
+
const key = env.VIMP_ROOM_SECRET_KEY;
|
|
61
|
+
|
|
62
|
+
if (key) {
|
|
63
|
+
if (Buffer.byteLength(key) < ROOM_SECRET_KEY_MIN_BYTES) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`VIMP_ROOM_SECRET_KEY: at least ${ROOM_SECRET_KEY_MIN_BYTES} bytes required`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { key, ephemeral: false };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (isProduction) {
|
|
73
|
+
throw new Error('VIMP_ROOM_SECRET_KEY must be set in production');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { key: randomBytes(ROOM_SECRET_KEY_MIN_BYTES), ephemeral: true };
|
|
77
|
+
}
|
|
78
|
+
|
|
42
79
|
/**
|
|
43
80
|
* Настройки комнаты dedicated-сервера: VIMP_DEDICATED_ROOM — JSON-объект
|
|
44
81
|
* (map, maxPlayers, roundTime, mapTime, friendlyFire, seed). Мусор в
|
|
@@ -64,6 +64,30 @@ export default {
|
|
|
64
64
|
maxLatency: 1000,
|
|
65
65
|
},
|
|
66
66
|
|
|
67
|
+
// возобновление сессии (host-migration этап 4). Сколько хост держит место
|
|
68
|
+
// участника, чей транспорт оборвался без LEAVE: слот занят, актор в мире,
|
|
69
|
+
// клавиши отпущены. Включает его только Worker лобби — dedicated и
|
|
70
|
+
// standalone поднимают PortMachine без этой опции (0 — снять сразу)
|
|
71
|
+
resumeGraceMs: 20000,
|
|
72
|
+
|
|
73
|
+
// сколько возобновляющее соединение может молчать до RESUME_REQUEST
|
|
74
|
+
resumeRequestTimeoutMs: 5000,
|
|
75
|
+
|
|
76
|
+
// промоушен преемника (host-migration этап 7.4): сколько матч, поднятый из
|
|
77
|
+
// контрольной точки, стоит на паузе, ожидая возврата людей точки, прежде
|
|
78
|
+
// чем стартовать без них (их место держится ещё resumeGraceMs)
|
|
79
|
+
resumeWaitMs: 3000,
|
|
80
|
+
|
|
81
|
+
// контрольная точка хоста (host-migration этап 5). Предел распакованного
|
|
82
|
+
// размера чужой точки — защита от «zip-бомбы»: сырой дамп tanks ~420 КБ,
|
|
83
|
+
// запас на порядок с лишним
|
|
84
|
+
maxCheckpointBytes: 8 * 1024 * 1024,
|
|
85
|
+
|
|
86
|
+
// сколько эстафета Worker'ов ждёт финальной синхронизации профилей
|
|
87
|
+
// (flushAll) перед отдачей состояния новому Worker'у; дольше — отдаёт
|
|
88
|
+
// как есть, неотправленное едет в состоянии
|
|
89
|
+
handoffFlushTimeoutMs: 3000,
|
|
90
|
+
|
|
67
91
|
// порог допустимого бездействия перед киком
|
|
68
92
|
// если null — кик отключен
|
|
69
93
|
idleKickTimeout: {
|
package/src/config/lobby.js
CHANGED
|
@@ -5,6 +5,39 @@ export default {
|
|
|
5
5
|
// REST-эндпоинт мастера со списком серверов (GET /servers)
|
|
6
6
|
serversUrl: '/servers',
|
|
7
7
|
|
|
8
|
+
// комната по прямому id (GET /rooms/:roomId, host-migration этап 3):
|
|
9
|
+
// прямая ссылка #/<gameId>/<roomId> проверяется до входа
|
|
10
|
+
roomUrl: roomId => `/rooms/${encodeURIComponent(roomId)}`,
|
|
11
|
+
|
|
12
|
+
// быстрая игра (GET /quickplay/:gameId, ревью F16): лучшая комната игры
|
|
13
|
+
// от мастера вместо всего списка; недоступен — GET /servers?search
|
|
14
|
+
quickPlayUrl: gameId => `/quickplay/${encodeURIComponent(gameId)}`,
|
|
15
|
+
|
|
16
|
+
// быстрая игра по ссылке #/<gameId>: подходящей комнаты нет — создать
|
|
17
|
+
// свою с настройками формы по умолчанию (false — показать лобби этой игры);
|
|
18
|
+
// createDelayMinMs…createDelayMaxMs — случайная пауза перед созданием и
|
|
19
|
+
// повторный GET /servers (host-migration этап 7): гости закрытой комнаты
|
|
20
|
+
// уходят в быструю игру разом, и без паузы каждый создал бы свою
|
|
21
|
+
quickPlay: {
|
|
22
|
+
autoCreate: true,
|
|
23
|
+
createDelayMinMs: 500,
|
|
24
|
+
createDelayMaxMs: 2000,
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
// меню комнаты внутри матча (только лобби-режим, roomMenu.pug)
|
|
28
|
+
roomMenu: {
|
|
29
|
+
elems: {
|
|
30
|
+
// кнопка меню живёт в панели, справа от таблицы
|
|
31
|
+
panelId: 'panel',
|
|
32
|
+
menuId: 'room-menu',
|
|
33
|
+
toggleId: 'room-menu-toggle',
|
|
34
|
+
listId: 'room-menu-list',
|
|
35
|
+
leaveId: 'room-menu-leave',
|
|
36
|
+
handoverId: 'room-menu-handover',
|
|
37
|
+
statusId: 'room-menu-status',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
|
|
8
41
|
// каталог игр мастера (Этап 6.3, GameCatalog): roomDefaults формы создания
|
|
9
42
|
// комнаты и ClientPlugin берутся отсюда вместо статической композиции
|
|
10
43
|
gamesManifestUrl: '/games/manifest.json',
|
|
@@ -31,6 +64,10 @@ export default {
|
|
|
31
64
|
// (деплой игры мог обновиться независимо от деплоя движка)
|
|
32
65
|
game: {
|
|
33
66
|
manifestUrl: gameId => `/games/${gameId}/manifest.json`,
|
|
67
|
+
// манифест конкретной версии: прогрев преемника (host-migration этап 6)
|
|
68
|
+
// поднимает ту версию игры, что крутится в комнате
|
|
69
|
+
versionManifestUrl: (gameId, version) =>
|
|
70
|
+
`/games/${gameId}/${encodeURIComponent(version)}/manifest.json`,
|
|
34
71
|
},
|
|
35
72
|
|
|
36
73
|
// манифест worker-бандла мастера (Этап 5.2): Worker комнаты создаётся по
|
|
@@ -141,6 +178,123 @@ export default {
|
|
|
141
178
|
maxDelay: 30000,
|
|
142
179
|
},
|
|
143
180
|
|
|
181
|
+
// установка P2P-соединения с хостом (host-migration этап 4): каналы не
|
|
182
|
+
// открылись за connectTimeoutMs — попытка считается провалившейся;
|
|
183
|
+
// offerRetryMs — пауза перед повтором оффера, отклонённого мастером на
|
|
184
|
+
// время плановой передачи хоста (error migrating, этап 8)
|
|
185
|
+
webrtc: {
|
|
186
|
+
connectTimeoutMs: 10000,
|
|
187
|
+
offerRetryMs: 1000,
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
// миграция хоста (host-migration этап 6; значения — замеры этапа 0):
|
|
191
|
+
// checkpointIntervalMs — период контрольных точек хоста для беты (поток
|
|
192
|
+
// ≤ ~150 КБ/с, serialize ≤ 25 % бюджета тика); standbyChunkBytes — кусок
|
|
193
|
+
// точки в канале standby (≤ минимального maxMessageSize с запасом);
|
|
194
|
+
// standbyHighWaterBytes — bufferedAmount, выше которого периодическая
|
|
195
|
+
// точка пропускается; standbyStatusIntervalMs — как часто бета сообщает
|
|
196
|
+
// мастеру свою последнюю точку (с её возрастом); maxRestoreAgeMs — точку,
|
|
197
|
+
// полученную раньше, преемник не поднимает (promote_failed → холодный
|
|
198
|
+
// старт; больше master:room:checkpointMaxAgeMs на дорогу статуса);
|
|
199
|
+
// standbyReopenDelayMs…standbyReopenMaxDelayMs — экспоненциальная пауза
|
|
200
|
+
// перед повторным открытием канала standby, закрывшегося при живом пире
|
|
201
|
+
// беты; finalWaitMs — сколько бета при плановой
|
|
202
|
+
// передаче (этап 8) ждёт финальную точку замороженного хоста, прежде чем
|
|
203
|
+
// взять последнюю периодическую; handoffSlowMs — нет handoff_go за это
|
|
204
|
+
// время — «медленная связь» (передача продолжается); handoffDeadlineMs —
|
|
205
|
+
// общий дедлайн плановой передачи от handoff_begin (master:room:
|
|
206
|
+
// handoffTimeoutMs + запас на дорогу ответа мастера); deferMaxMs — потолок
|
|
207
|
+
// ожидания границы раунда передачей в игре без migration.midRound (этап 8d);
|
|
208
|
+
// peersReportIntervalMs — период повтора room_peers хоста мастеру (кто
|
|
209
|
+
// подключён по WebRTC; при смене состава отчёт уходит сразу, с дебаунсом);
|
|
210
|
+
// minTokenLifetimeMs — вкладка, чей вход истечёт раньше, не объявляет
|
|
211
|
+
// canHost и отказывается от промоушена (токен предъявляется мастеру
|
|
212
|
+
// посреди матча, продления нет); tokenHandoffLeadMs — за столько до
|
|
213
|
+
// истечения входа хост с бетой сам передаёт роль (плановая передача на
|
|
214
|
+
// границе раунда); tokenHandoffRetryMs — через сколько повторить её, если
|
|
215
|
+
// беты ещё нет, идёт эстафета Worker'ов или передача сорвалась (повторы —
|
|
216
|
+
// до истечения входа); leaveFlushTimeoutMs — сколько «Leave
|
|
217
|
+
// server» хоста без людей ждёт записи очков участников перед закрытием
|
|
218
|
+
// комнаты (HostController.shutdown)
|
|
219
|
+
migration: {
|
|
220
|
+
checkpointIntervalMs: 500,
|
|
221
|
+
standbyChunkBytes: 65536,
|
|
222
|
+
standbyHighWaterBytes: 1024 * 1024,
|
|
223
|
+
standbyStatusIntervalMs: 5000,
|
|
224
|
+
maxRestoreAgeMs: 15000,
|
|
225
|
+
standbyReopenDelayMs: 1000,
|
|
226
|
+
standbyReopenMaxDelayMs: 10000,
|
|
227
|
+
finalWaitMs: 3000,
|
|
228
|
+
handoffSlowMs: 3000,
|
|
229
|
+
handoffDeadlineMs: 10000,
|
|
230
|
+
deferMaxMs: 30000,
|
|
231
|
+
peersReportIntervalMs: 15000,
|
|
232
|
+
minTokenLifetimeMs: 600000,
|
|
233
|
+
tokenHandoffLeadMs: 300000,
|
|
234
|
+
tokenHandoffRetryMs: 5000,
|
|
235
|
+
leaveFlushTimeoutMs: 3000,
|
|
236
|
+
|
|
237
|
+
// автотриггеры передачи (этап 9b, HostHealthPolicy; сэмпл — сообщение
|
|
238
|
+
// health Worker'а раз в ~1 с): мягкая перегрузка — среднее tickRate за
|
|
239
|
+
// overloadWindowMs ниже overloadTickRate (передача ждёт границы раунда);
|
|
240
|
+
// жёсткая — среднее за criticalWindowMs ниже criticalTickRate или
|
|
241
|
+
// lostMs > 0 у lostWindows сэмплов подряд (сразу); отложенная
|
|
242
|
+
// отменяется, когда все сэмплы за recoverWindowMs выше recoverTickRate
|
|
243
|
+
// (гистерезис); hiddenHandoffMs — скрытая вкладка хоста (этап 0:
|
|
244
|
+
// троттлинг Worker'а за 1–3 с); autoHandoffCooldownMs — между
|
|
245
|
+
// авто-передачами вкладки; minHostTenureMs — не отдавать роль, только
|
|
246
|
+
// что её получив; enabled — общий выключатель
|
|
247
|
+
auto: {
|
|
248
|
+
enabled: true,
|
|
249
|
+
overloadTickRate: 100,
|
|
250
|
+
overloadWindowMs: 5000,
|
|
251
|
+
criticalTickRate: 60,
|
|
252
|
+
criticalWindowMs: 3000,
|
|
253
|
+
lostWindows: 3,
|
|
254
|
+
recoverTickRate: 110,
|
|
255
|
+
recoverWindowMs: 5000,
|
|
256
|
+
hiddenHandoffMs: 1500,
|
|
257
|
+
autoHandoffCooldownMs: 90000,
|
|
258
|
+
minHostTenureMs: 30000,
|
|
259
|
+
// этап 9c: host_health мастеру (правило сетевого лага) не чаще
|
|
260
|
+
// hostHealthIntervalMs; гость шлёт свой FPS рендера (caps.fps) раз в
|
|
261
|
+
// fpsReportIntervalMs. Просьбу мастера request_handoff хост
|
|
262
|
+
// выполняет, только если enabled
|
|
263
|
+
hostHealthIntervalMs: 2000,
|
|
264
|
+
fpsReportIntervalMs: 10000,
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
// супервизор сессии гостя (host-migration этап 4, SessionSupervisor):
|
|
269
|
+
// reconnectWindowMs — сколько после обрыва транспорта пытаться вернуться в
|
|
270
|
+
// матч (повторы с бэкоффом reconnectBaseDelayMs…reconnectMaxDelayMs);
|
|
271
|
+
// hostSilenceMs — молчание хоста в игре, после которого транспорт
|
|
272
|
+
// считается мёртвым (кадры идут ~30/с, PING — раз в 3 с);
|
|
273
|
+
// migrationWaitMs — сколько после host_migrating ждать host_changed, затем
|
|
274
|
+
// комната считается закрытой (быструю игру); migrationPollMs — период
|
|
275
|
+
// повторного GET /rooms/:roomId, пока комната по ссылке в 'migrating';
|
|
276
|
+
// host_migrating.waitMs мастера (сколько он ещё ищет преемника) ожидание
|
|
277
|
+
// продлевает, но не укорачивает; linkWaitMaxMs — крайний срок ожидания
|
|
278
|
+
// комнаты по ссылке, пока она меняет хоста; resumeSilenceGraceMs — фора
|
|
279
|
+
// сторожку тишины после возобновления до первого кадра: восстановленный
|
|
280
|
+
// матч стоит до hostDefaults.resumeWaitMs, ожидая остальных. Связки с
|
|
281
|
+
// таймингами мастера и Worker'а — tests/config/migrationTimings.test.js;
|
|
282
|
+
// joinRetryWindowMs — сколько гость повторяет join_room на unknownRoom
|
|
283
|
+
// после рестарта мастера, пока хост не вернёт комнату reclaim_host (больше
|
|
284
|
+
// reconnect.maxDelay — бэкоффа сигналинга хоста — с запасом на его
|
|
285
|
+
// reclaim_host)
|
|
286
|
+
session: {
|
|
287
|
+
reconnectWindowMs: 15000,
|
|
288
|
+
reconnectBaseDelayMs: 500,
|
|
289
|
+
reconnectMaxDelayMs: 4000,
|
|
290
|
+
hostSilenceMs: 3000,
|
|
291
|
+
migrationWaitMs: 40000,
|
|
292
|
+
migrationPollMs: 1000,
|
|
293
|
+
linkWaitMaxMs: 90000,
|
|
294
|
+
resumeSilenceGraceMs: 3000,
|
|
295
|
+
joinRetryWindowMs: 45000,
|
|
296
|
+
},
|
|
297
|
+
|
|
144
298
|
// приёмник выгрузок отладочного контура (этап 6 плана plan/done/ai-debug):
|
|
145
299
|
// маршрут поднимается мастером только в dev, в проде вернёт 404
|
|
146
300
|
debugReportUrl: '/debug/report',
|
|
@@ -163,7 +317,6 @@ export default {
|
|
|
163
317
|
searchId: 'lobby-search',
|
|
164
318
|
moreId: 'lobby-more',
|
|
165
319
|
emptyId: 'lobby-empty',
|
|
166
|
-
nameId: 'lobby-name',
|
|
167
320
|
hostBtnId: 'lobby-host',
|
|
168
321
|
// строка отказа под кнопкой: загрузка ClientPlugin выбранной игры может
|
|
169
322
|
// не удаться, и лобби обязано остаться рабочим
|
|
@@ -327,8 +480,6 @@ export default {
|
|
|
327
480
|
// раунда-карты/огонь по своим/карта по умолчанию — из roomDefaults
|
|
328
481
|
// манифеста активной игры (Этап 6.3), не бандлятся здесь
|
|
329
482
|
create: {
|
|
330
|
-
defaultName: 'My Server',
|
|
331
|
-
|
|
332
483
|
// каталог платформы пуст: реестр ещё ничего не одобрил либо модератор
|
|
333
484
|
// снял с раздачи последнюю игру. Комнату создавать не на чем, но лобби
|
|
334
485
|
// живо — и текст называет то единственное, что выводит его из этого
|