vimp-engine 0.35.6 → 0.37.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/Chat.js +19 -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 +676 -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/chatMessages.js +45 -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 +2 -2
- 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 +29 -3
- 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 +0 -20
- 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')
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Английские тексты движковых кодов чата по умолчанию (группы s, v, m, c, n);
|
|
2
|
+
// индекс = номер кода. Лежат здесь, а не в clientDefaults: при слиянии
|
|
3
|
+
// конфига массивы заменяются целиком, и массив `s` игры стёр бы умолчания.
|
|
4
|
+
// Правило перекрытия: строка игры по тому же индексу важнее умолчания;
|
|
5
|
+
// '' — игра заглушает сообщение; null/нет индекса — берётся умолчание.
|
|
6
|
+
// Новый код движка добавляется в ENGINE_MESSAGE_CODES (host/meta/modules/chat/
|
|
7
|
+
// systemMessages.js), сюда и в RESERVED правила B8 одним изменением —
|
|
8
|
+
// за этим следит tests/config/chatMessages.test.js.
|
|
9
|
+
export default {
|
|
10
|
+
s: [
|
|
11
|
+
'Team {0} is full. Your current team: {1}',
|
|
12
|
+
'Your team: {0}',
|
|
13
|
+
'Your new team: {0}',
|
|
14
|
+
'Your new status: spectator',
|
|
15
|
+
'{0} killed {1}',
|
|
16
|
+
'{0} joined the game',
|
|
17
|
+
'{0} left the game',
|
|
18
|
+
'Host changed',
|
|
19
|
+
'You are no longer the host (connection lost)',
|
|
20
|
+
'Host changed: the previous host was lagging',
|
|
21
|
+
'Host changed: the previous host went inactive',
|
|
22
|
+
'Host changed: the previous host had a poor connection',
|
|
23
|
+
],
|
|
24
|
+
v: [
|
|
25
|
+
'A vote has been created',
|
|
26
|
+
'Voting has started',
|
|
27
|
+
'Your vote has been accepted',
|
|
28
|
+
'Voting is temporarily unavailable',
|
|
29
|
+
'Vote passed',
|
|
30
|
+
'Vote failed',
|
|
31
|
+
'Usage: /changehost',
|
|
32
|
+
'You are the host — use “Hand over host” in the room menu',
|
|
33
|
+
'No connection to the master server',
|
|
34
|
+
'A host vote was held recently',
|
|
35
|
+
'No other player can host',
|
|
36
|
+
'A host vote is already in progress',
|
|
37
|
+
'A host vote is not possible right now',
|
|
38
|
+
'Vote to change host passed ({0}/{1})',
|
|
39
|
+
'Vote to change host failed ({0}/{1})',
|
|
40
|
+
'Host vote cancelled',
|
|
41
|
+
],
|
|
42
|
+
m: ['Current map: {0}', 'Next map: {0}'],
|
|
43
|
+
c: ['Command not found', 'Your rank: {0}'],
|
|
44
|
+
n: ['Invalid name', '{0} changed name to {1}'],
|
|
45
|
+
};
|
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: {
|