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
package/src/host/host.worker.js
CHANGED
|
@@ -6,14 +6,18 @@
|
|
|
6
6
|
// сюда приходят уже разобранные пакеты клиентов, обратно уходят wire-кадры
|
|
7
7
|
// (JSON-строки и бинарные ArrayBuffer'ы через Transferable).
|
|
8
8
|
//
|
|
9
|
-
// Сам хендшейк клиента (порты 0–
|
|
9
|
+
// Сам хендшейк клиента (порты 0–10) живёт в изоморфной ./PortMachine.js —
|
|
10
10
|
// этот файл только адаптер: postMessage-транспорт, лобби-стратегия
|
|
11
11
|
// идентичности и свитч сообщений главного потока.
|
|
12
12
|
|
|
13
13
|
import authClientConfig from '../config/authClient.js';
|
|
14
14
|
import lobbyConfig from '../config/lobby.js';
|
|
15
15
|
import wsports from '../config/wsports.js';
|
|
16
|
-
import {
|
|
16
|
+
import { encodeCheckpoint, decodeCheckpoint } from '../lib/checkpointCodec.js';
|
|
17
|
+
import {
|
|
18
|
+
createHostRuntime,
|
|
19
|
+
preloadHostRuntime,
|
|
20
|
+
} from '../lib/createHostRuntime.js';
|
|
17
21
|
import PortMachine from './PortMachine.js';
|
|
18
22
|
import { createTokenIdentity } from './identity.js';
|
|
19
23
|
|
|
@@ -23,6 +27,10 @@ const PS_TECH_INFORM_DATA = wsports.server.TECH_INFORM_DATA;
|
|
|
23
27
|
let host = null;
|
|
24
28
|
let portMachine = null;
|
|
25
29
|
|
|
30
|
+
// комната закрывается (shutdown): новых не пускаем — иначе вошедший вылетел
|
|
31
|
+
// бы через секунды вместе с комнатой
|
|
32
|
+
let shuttingDown = false;
|
|
33
|
+
|
|
26
34
|
// эстафета Worker'ов (Этап 5.2): socketId → gameId участников, восстановленных
|
|
27
35
|
// из handoff-меты — их порт-машины поднимаются минуя хендшейк
|
|
28
36
|
let handoffClients = null;
|
|
@@ -68,21 +76,81 @@ function makeWorkerSocket(socketId) {
|
|
|
68
76
|
};
|
|
69
77
|
}
|
|
70
78
|
|
|
79
|
+
// участники эстафеты внутри вкладки: v3 — плоский humans, v4 — формат
|
|
80
|
+
// контрольной точки (participants.humans)
|
|
81
|
+
function handoffHumans(handoff) {
|
|
82
|
+
return handoff.participants?.humans ?? handoff.humans ?? [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// готовая контрольная точка уходит главному потоку списком переноса — без
|
|
86
|
+
// копии: 2 точки в секунду не должны стоить сборщику мусора лишних буферов.
|
|
87
|
+
// Буфер кодека принадлежит результату целиком (не view на память wasm)
|
|
88
|
+
function postCheckpoint({ meta, core, final }) {
|
|
89
|
+
encodeCheckpoint(meta, core)
|
|
90
|
+
.then(bytes => {
|
|
91
|
+
const owned =
|
|
92
|
+
bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength
|
|
93
|
+
? bytes
|
|
94
|
+
: bytes.slice();
|
|
95
|
+
|
|
96
|
+
self.postMessage(
|
|
97
|
+
{
|
|
98
|
+
type: 'checkpoint',
|
|
99
|
+
checkpointId: meta.checkpointId,
|
|
100
|
+
seq: meta.seq,
|
|
101
|
+
createdAt: meta.createdAt,
|
|
102
|
+
final,
|
|
103
|
+
mode: meta.mode,
|
|
104
|
+
// игра точки — в дескриптор канала standby: бете не нужно
|
|
105
|
+
// распаковывать точку ради версии игры (ревью F15)
|
|
106
|
+
game: meta.room?.game
|
|
107
|
+
? { id: meta.room.game.id, version: meta.room.game.version }
|
|
108
|
+
: null,
|
|
109
|
+
bytes: owned,
|
|
110
|
+
},
|
|
111
|
+
[owned.buffer],
|
|
112
|
+
);
|
|
113
|
+
})
|
|
114
|
+
.catch(e => {
|
|
115
|
+
self.postMessage({
|
|
116
|
+
type: 'diagnostic',
|
|
117
|
+
kind: 'checkpoint',
|
|
118
|
+
message: e && e.message ? e.message : String(e),
|
|
119
|
+
stack: e && typeof e.stack === 'string' ? e.stack : null,
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
71
124
|
// инициализация хоста: HostPlugin игры (динамический import по
|
|
72
125
|
// GameManifest, Этап 6.4), ядро, мета, игровой цикл. handoff — состояние
|
|
73
126
|
// эстафеты Worker'ов (Этап 5.2): комната восстанавливается вместо
|
|
74
|
-
// холодного старта, порт-машины клиентов поднимутся минуя
|
|
75
|
-
|
|
127
|
+
// холодного старта, порт-машины клиентов поднимутся минуя хендшейк.
|
|
128
|
+
// checkpoint — сжатая контрольная точка (host-migration этап 5): матч
|
|
129
|
+
// поднимается на паузе, люди ждут RESUME, старт — start_after_restore
|
|
130
|
+
async function onInit(room, handoff = null, checkpointBytes = null, seqFloor) {
|
|
131
|
+
const checkpoint = checkpointBytes
|
|
132
|
+
? await decodeCheckpoint(checkpointBytes)
|
|
133
|
+
: null;
|
|
134
|
+
|
|
76
135
|
// общая с headless-runner'ом сборка (lib/createHostRuntime.js) — чтобы
|
|
77
136
|
// отладочный прогон крутил ровно тот код, что и прод
|
|
78
137
|
const runtime = await createHostRuntime(room, {
|
|
79
138
|
hostOptions: {
|
|
80
|
-
|
|
81
|
-
|
|
139
|
+
// map_changed остаётся для главного потока до host-migration этапа 2
|
|
140
|
+
onMapChange: mapName =>
|
|
141
|
+
self.postMessage({ type: 'map_changed', mapName }),
|
|
142
|
+
onLobbyInfoChange: info => self.postMessage({ type: 'lobby_info', info }),
|
|
143
|
+
handoff: checkpoint ? null : handoff,
|
|
144
|
+
checkpoint,
|
|
145
|
+
seqFloor: Number(seqFloor) || 0,
|
|
82
146
|
},
|
|
83
147
|
});
|
|
84
148
|
|
|
85
149
|
host = runtime.host;
|
|
150
|
+
host.setCheckpointSink(postCheckpoint);
|
|
151
|
+
// здоровье хоста (host-migration этап 9a): главный поток решает, не пора
|
|
152
|
+
// ли отдать роль
|
|
153
|
+
host.setHealthSink(health => self.postMessage({ type: 'health', health }));
|
|
86
154
|
|
|
87
155
|
// в лобби личность игрока — claim identity-токена, проверенного по JWKS
|
|
88
156
|
// мастера (Этап B3); свободного ввода имени в форме игры нет
|
|
@@ -96,25 +164,44 @@ async function onInit(room, handoff = null) {
|
|
|
96
164
|
jwksUrl: lobbyConfig.auth.jwksUrl,
|
|
97
165
|
issuer: authClientConfig.issuer,
|
|
98
166
|
}),
|
|
167
|
+
// возобновление сессии (host-migration этап 4) — только в лобби:
|
|
168
|
+
// оборвавшийся гость держит место resumeGraceMs и возвращается в него
|
|
169
|
+
resumeGraceMs: runtime.game.resumeGraceMs,
|
|
170
|
+
resumeRequestTimeoutMs: runtime.game.resumeRequestTimeoutMs,
|
|
99
171
|
});
|
|
100
172
|
|
|
101
173
|
const seed = runtime.seed;
|
|
102
174
|
|
|
103
|
-
if (handoff) {
|
|
104
|
-
handoffClients = new Map(
|
|
175
|
+
if (handoff && !checkpoint) {
|
|
176
|
+
handoffClients = new Map(
|
|
177
|
+
handoffHumans(handoff).map(h => [h.socketId, h.gameId]),
|
|
178
|
+
);
|
|
105
179
|
}
|
|
106
180
|
|
|
107
181
|
// мастеру нужна фактическая карта комнаты (после эстафеты — восстановленная)
|
|
108
|
-
self.postMessage({
|
|
182
|
+
self.postMessage({
|
|
183
|
+
type: 'ready',
|
|
184
|
+
mapName: host.currentMap,
|
|
185
|
+
lobbyInfo: host.lobbyInfo,
|
|
186
|
+
seed,
|
|
187
|
+
});
|
|
109
188
|
}
|
|
110
189
|
|
|
111
190
|
// новое подключение клиента: участник из handoff-меты уже восстановлен в
|
|
112
191
|
// HostGame — его порт-машина поднимается сразу в игровом состоянии
|
|
113
|
-
|
|
192
|
+
// resume — переподключение гостя (host-migration этап 4): порт-машина ждёт
|
|
193
|
+
// RESUME_REQUEST вместо хендшейка
|
|
194
|
+
function onConnect(socketId, resume = false) {
|
|
114
195
|
if (!portMachine) {
|
|
115
196
|
return;
|
|
116
197
|
}
|
|
117
198
|
|
|
199
|
+
// закрытие без кода: в лобби-режиме гость уходит искать другую комнату
|
|
200
|
+
if (shuttingDown) {
|
|
201
|
+
self.postMessage({ type: 'close_client', socketId });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
118
205
|
const restoredGameId = handoffClients?.get(socketId);
|
|
119
206
|
|
|
120
207
|
if (restoredGameId !== undefined) {
|
|
@@ -123,7 +210,7 @@ function onConnect(socketId) {
|
|
|
123
210
|
return;
|
|
124
211
|
}
|
|
125
212
|
|
|
126
|
-
portMachine.connect(socketId);
|
|
213
|
+
portMachine.connect(socketId, { resume });
|
|
127
214
|
}
|
|
128
215
|
|
|
129
216
|
// отладочные действия хоста (этап 6): запись живого матча в формат сценария
|
|
@@ -171,7 +258,7 @@ self.onmessage = async event => {
|
|
|
171
258
|
switch (msg.type) {
|
|
172
259
|
case 'init':
|
|
173
260
|
try {
|
|
174
|
-
await onInit(msg.room, msg.handoff);
|
|
261
|
+
await onInit(msg.room, msg.handoff, msg.checkpoint, msg.seqFloor);
|
|
175
262
|
} catch (e) {
|
|
176
263
|
// сбой загрузки WASM/конфига/handoff-меты — сообщить главному
|
|
177
264
|
// потоку, не виснуть (при эстафете тот возобновит старый Worker)
|
|
@@ -185,8 +272,29 @@ self.onmessage = async event => {
|
|
|
185
272
|
}
|
|
186
273
|
break;
|
|
187
274
|
|
|
275
|
+
// прогрев Worker'а преемника (host-migration этап 6): плагин и wasm
|
|
276
|
+
// готовы, матча нет — его поднимет init с контрольной точкой (этап 7)
|
|
277
|
+
case 'preload':
|
|
278
|
+
try {
|
|
279
|
+
const { wasmCompiled } = await preloadHostRuntime(msg.room);
|
|
280
|
+
|
|
281
|
+
self.postMessage({
|
|
282
|
+
type: 'preloaded',
|
|
283
|
+
gameId: msg.room?.game?.id ?? null,
|
|
284
|
+
gameVersion: msg.room?.game?.version ?? null,
|
|
285
|
+
wasmCompiled,
|
|
286
|
+
});
|
|
287
|
+
} catch (e) {
|
|
288
|
+
self.postMessage({
|
|
289
|
+
type: 'error',
|
|
290
|
+
message: e && e.message ? e.message : String(e),
|
|
291
|
+
stack: e && typeof e.stack === 'string' ? e.stack : null,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
break;
|
|
295
|
+
|
|
188
296
|
case 'connect':
|
|
189
|
-
onConnect(msg.socketId);
|
|
297
|
+
onConnect(msg.socketId, msg.resume === true);
|
|
190
298
|
break;
|
|
191
299
|
|
|
192
300
|
case 'message':
|
|
@@ -201,10 +309,20 @@ self.onmessage = async event => {
|
|
|
201
309
|
host?.updateMaps(msg.maps);
|
|
202
310
|
break;
|
|
203
311
|
|
|
204
|
-
// мастер подтвердил регистрацию комнаты
|
|
205
|
-
//
|
|
312
|
+
// мастер подтвердил регистрацию комнаты — roomId+секрет эпохи нужны
|
|
313
|
+
// PlayerDataSync для атрибуции последующих rank/state-flush
|
|
314
|
+
case 'set_room':
|
|
315
|
+
host?.setRoom({
|
|
316
|
+
roomId: msg.roomId,
|
|
317
|
+
roomSecret: msg.roomSecret,
|
|
318
|
+
epoch: msg.epoch,
|
|
319
|
+
});
|
|
320
|
+
break;
|
|
321
|
+
|
|
322
|
+
// имя того же сообщения у главного потока до host-migration этапа 2:
|
|
323
|
+
// страница, загруженная до деплоя, поднимает Worker по свежему манифесту
|
|
206
324
|
case 'set_host_id':
|
|
207
|
-
host?.
|
|
325
|
+
host?.setRoom({ roomId: msg.hostId, roomSecret: msg.hostSecret });
|
|
208
326
|
break;
|
|
209
327
|
|
|
210
328
|
// отладочный контур (этап 6 плана plan/done/ai-debug): единственный вход в
|
|
@@ -223,6 +341,11 @@ self.onmessage = async event => {
|
|
|
223
341
|
);
|
|
224
342
|
break;
|
|
225
343
|
|
|
344
|
+
// плановая передача хоста вытесняет эстафету, ещё ждущую границы раунда
|
|
345
|
+
case 'cancel_handoff':
|
|
346
|
+
host?.cancelHandoff();
|
|
347
|
+
break;
|
|
348
|
+
|
|
226
349
|
// новый Worker не поднялся — продолжаем жить на этой версии
|
|
227
350
|
case 'resume':
|
|
228
351
|
host?.resumeAfterHandoff();
|
|
@@ -233,5 +356,88 @@ self.onmessage = async event => {
|
|
|
233
356
|
handoffClients = null;
|
|
234
357
|
host?.completeHandoff(new Set(portMachine ? portMachine.socketIds : []));
|
|
235
358
|
break;
|
|
359
|
+
|
|
360
|
+
// контрольные точки (host-migration этап 5)
|
|
361
|
+
|
|
362
|
+
// периодические точки: снимаются на границе кадра не чаще intervalMs
|
|
363
|
+
case 'checkpoint_start':
|
|
364
|
+
host?.startCheckpoints(msg.intervalMs);
|
|
365
|
+
break;
|
|
366
|
+
|
|
367
|
+
case 'checkpoint_stop':
|
|
368
|
+
host?.stopCheckpoints();
|
|
369
|
+
break;
|
|
370
|
+
|
|
371
|
+
// одна точка на ближайшей границе кадра (final — финальная перед
|
|
372
|
+
// плановой передачей)
|
|
373
|
+
case 'checkpoint_request':
|
|
374
|
+
host?.requestCheckpoint({ final: msg.final === true });
|
|
375
|
+
break;
|
|
376
|
+
|
|
377
|
+
// матч поднят из точки: запустить цикл и таймеры. waitForResume —
|
|
378
|
+
// промоушен преемника (этап 7.4): старт, когда вернулись все люди точки
|
|
379
|
+
// или истёк resumeWaitMs; не вернувшимся — ожидание resumeGraceMs.
|
|
380
|
+
// reason (этап 9) — причина передачи для сообщения игрокам; старый
|
|
381
|
+
// главный поток её не шлёт
|
|
382
|
+
case 'start_after_restore':
|
|
383
|
+
if (msg.waitForResume === true) {
|
|
384
|
+
host?.startAfterResume(ids => portMachine?.startGraceFor(ids), {
|
|
385
|
+
reason: typeof msg.reason === 'string' ? msg.reason : null,
|
|
386
|
+
});
|
|
387
|
+
} else {
|
|
388
|
+
host?.startAfterRestore();
|
|
389
|
+
}
|
|
390
|
+
break;
|
|
391
|
+
|
|
392
|
+
// плановая передача хоста (этап 8d): дождаться границы раунда (у игры
|
|
393
|
+
// с migration.midRound — ответ сразу)
|
|
394
|
+
case 'round_boundary_wait':
|
|
395
|
+
host?.awaitRoundBoundary(() =>
|
|
396
|
+
self.postMessage({ type: 'round_boundary' }),
|
|
397
|
+
);
|
|
398
|
+
break;
|
|
399
|
+
|
|
400
|
+
case 'round_boundary_cancel':
|
|
401
|
+
host?.cancelRoundBoundary();
|
|
402
|
+
break;
|
|
403
|
+
|
|
404
|
+
case 'freeze':
|
|
405
|
+
host?.freeze();
|
|
406
|
+
break;
|
|
407
|
+
|
|
408
|
+
case 'unfreeze':
|
|
409
|
+
host?.unfreeze();
|
|
410
|
+
break;
|
|
411
|
+
|
|
412
|
+
// хост закрывает комнату («Leave server» последнего человека): закрыть
|
|
413
|
+
// игры участников и дождаться записи профилей — не дольше timeoutMs
|
|
414
|
+
case 'shutdown': {
|
|
415
|
+
const timeoutMs =
|
|
416
|
+
Number(msg.timeoutMs) > 0 ? Number(msg.timeoutMs) : 3000;
|
|
417
|
+
let timer = null;
|
|
418
|
+
|
|
419
|
+
shuttingDown = true;
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
await Promise.race([
|
|
423
|
+
host?.destroy(),
|
|
424
|
+
new Promise(resolve => {
|
|
425
|
+
timer = setTimeout(resolve, timeoutMs);
|
|
426
|
+
}),
|
|
427
|
+
]);
|
|
428
|
+
} catch (e) {
|
|
429
|
+
self.postMessage({
|
|
430
|
+
type: 'diagnostic',
|
|
431
|
+
kind: 'shutdown',
|
|
432
|
+
message: e && e.message ? e.message : String(e),
|
|
433
|
+
stack: e && typeof e.stack === 'string' ? e.stack : null,
|
|
434
|
+
});
|
|
435
|
+
} finally {
|
|
436
|
+
clearTimeout(timer);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
self.postMessage({ type: 'shutdown_done' });
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
236
442
|
}
|
|
237
443
|
};
|
|
@@ -40,6 +40,8 @@ export default class SocketManager {
|
|
|
40
40
|
this._PORT_VOTE_DATA = ports.VOTE_DATA;
|
|
41
41
|
this._PORT_KEYSET_DATA = ports.KEYSET_DATA;
|
|
42
42
|
this._PORT_ACCOLADES_DATA = ports.ACCOLADES_DATA;
|
|
43
|
+
this._PORT_SESSION_DATA = ports.SESSION_DATA;
|
|
44
|
+
this._PORT_RESUME_RESULT = ports.RESUME_RESULT;
|
|
43
45
|
|
|
44
46
|
this._soundCues = soundCues;
|
|
45
47
|
this._initialVote = initialVote;
|
|
@@ -436,6 +438,24 @@ export default class SocketManager {
|
|
|
436
438
|
}
|
|
437
439
|
}
|
|
438
440
|
|
|
441
|
+
/**
|
|
442
|
+
* Отправка секрета возобновления сессии (host-migration этап 4).
|
|
443
|
+
* @param {string} socketId
|
|
444
|
+
* @param {Object} data - { resumeKey, gameId }.
|
|
445
|
+
*/
|
|
446
|
+
sendSessionData(socketId, data) {
|
|
447
|
+
this._send(socketId, this._PORT_SESSION_DATA, data);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Ответ на RESUME_REQUEST (host-migration этап 4).
|
|
452
|
+
* @param {string} socketId
|
|
453
|
+
* @param {Object} data - { ok: true, gameId, epoch } | { ok: false, reason }.
|
|
454
|
+
*/
|
|
455
|
+
sendResumeResult(socketId, data) {
|
|
456
|
+
this._send(socketId, this._PORT_RESUME_RESULT, data);
|
|
457
|
+
}
|
|
458
|
+
|
|
439
459
|
/**
|
|
440
460
|
* Отправка команды смены имени.
|
|
441
461
|
* @param {string} socketId
|
|
@@ -79,9 +79,15 @@ class RoundManager {
|
|
|
79
79
|
// идёт дальше, и второй раз `deaths` той же команде не пишется
|
|
80
80
|
this._wipedTeamIds = new Set();
|
|
81
81
|
this._removedPlayersList = []; // gameId игроков для удаления с полотна
|
|
82
|
+
// карта, подменённая игрой (overrideMapData) в клиентском виде, или null —
|
|
83
|
+
// контрольной точке она нужна отдельно от карты каталога
|
|
84
|
+
this._mapOverride = null;
|
|
85
|
+
this._baseMapData = null;
|
|
82
86
|
|
|
83
87
|
// эстафета Worker'ов (Этап 5.2): колбэк переноса на границе раунда
|
|
84
88
|
this._handoffCallback = null;
|
|
89
|
+
// плановая передача хоста ждёт границы раунда (host-migration этап 8d)
|
|
90
|
+
this._boundaryCallback = null;
|
|
85
91
|
}
|
|
86
92
|
|
|
87
93
|
get currentMap() {
|
|
@@ -92,6 +98,12 @@ class RoundManager {
|
|
|
92
98
|
return this._currentMapData;
|
|
93
99
|
}
|
|
94
100
|
|
|
101
|
+
// JSON текущей карты в виде каталога (до масштабирования и подмены игрой)
|
|
102
|
+
// — контрольная точка везёт его с собой: каталог преемника мог измениться
|
|
103
|
+
get baseMapData() {
|
|
104
|
+
return this._baseMapData;
|
|
105
|
+
}
|
|
106
|
+
|
|
95
107
|
get removedPlayersList() {
|
|
96
108
|
return this._removedPlayersList;
|
|
97
109
|
}
|
|
@@ -113,11 +125,15 @@ class RoundManager {
|
|
|
113
125
|
}
|
|
114
126
|
|
|
115
127
|
// подготавливает данные текущей карты (масштабирование) без пересоздания
|
|
116
|
-
// мира — общий шаг createMap и восстановления эстафеты (Этап 5.2)
|
|
117
|
-
|
|
128
|
+
// мира — общий шаг createMap и восстановления эстафеты (Этап 5.2).
|
|
129
|
+
// source — JSON карты из контрольной точки (host-migration этап 5): та
|
|
130
|
+
// самая карта, что лежит в дампе ядра, даже если каталог уже другой
|
|
131
|
+
_prepareMapData(source = this._maps[this._currentMap]) {
|
|
132
|
+
this._mapOverride = null;
|
|
133
|
+
this._baseMapData = source;
|
|
118
134
|
this._currentMapData = {
|
|
119
135
|
scale: this._mapScale,
|
|
120
|
-
...
|
|
136
|
+
...source,
|
|
121
137
|
};
|
|
122
138
|
|
|
123
139
|
// если нет индивидуального конструктора для создания карты
|
|
@@ -130,13 +146,58 @@ class RoundManager {
|
|
|
130
146
|
|
|
131
147
|
// восстанавливает карту после эстафеты Worker'ов (Этап 5.2): без
|
|
132
148
|
// пересоздания мира и рассылки клиентам — мир соберёт первый
|
|
133
|
-
// initiateNewRound после
|
|
134
|
-
|
|
149
|
+
// initiateNewRound после переноса, а при восстановлении посреди раунда
|
|
150
|
+
// (host-migration этап 5) он уже внутри дампа ядра: здесь только
|
|
151
|
+
// JS-сторона, ни createMap, ни load_map. scripted.createMap запоминает
|
|
152
|
+
// точки респауна и ботов в ядре не создаёт
|
|
153
|
+
restoreMap(mapName, { mapData = null, override = null } = {}) {
|
|
135
154
|
this._currentMap = mapName;
|
|
136
|
-
this._prepareMapData();
|
|
155
|
+
this._prepareMapData(mapData ?? this._maps[mapName]);
|
|
156
|
+
|
|
157
|
+
if (override) {
|
|
158
|
+
this.overrideMapData(override);
|
|
159
|
+
}
|
|
160
|
+
|
|
137
161
|
this._scripted.createMap(this._scaledMapData);
|
|
138
162
|
}
|
|
139
163
|
|
|
164
|
+
// состояние раунда для контрольной точки (host-migration этап 5)
|
|
165
|
+
serialize() {
|
|
166
|
+
return {
|
|
167
|
+
currentMap: this._currentMap,
|
|
168
|
+
override: this._mapOverride,
|
|
169
|
+
isRoundEnding: this._isRoundEnding,
|
|
170
|
+
wipedTeamIds: [...this._wipedTeamIds],
|
|
171
|
+
removedPlayers: [...this._removedPlayersList],
|
|
172
|
+
startMapNumber: this._startMapNumber,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// флаги раунда из контрольной точки (карту поднимает restoreMap)
|
|
177
|
+
restoreRound({
|
|
178
|
+
isRoundEnding = false,
|
|
179
|
+
wipedTeamIds = [],
|
|
180
|
+
removedPlayers = [],
|
|
181
|
+
startMapNumber = 0,
|
|
182
|
+
} = {}) {
|
|
183
|
+
this._isRoundEnding = isRoundEnding === true;
|
|
184
|
+
this._wipedTeamIds = new Set(wipedTeamIds);
|
|
185
|
+
this._removedPlayersList = [...removedPlayers];
|
|
186
|
+
this._startMapNumber = startMapNumber;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// отложенная смена карты: цель объявлена таймеру, чтобы контрольная точка
|
|
190
|
+
// могла её перенести (duration — остаток при восстановлении)
|
|
191
|
+
scheduleMapChange(mapName, duration) {
|
|
192
|
+
this._timerManager.startMapChangeDelay(
|
|
193
|
+
() => {
|
|
194
|
+
this._currentMap = mapName;
|
|
195
|
+
this.createMap();
|
|
196
|
+
},
|
|
197
|
+
{ targetMap: mapName, duration },
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
140
201
|
// создает карту
|
|
141
202
|
createMap() {
|
|
142
203
|
this._prepareMapData();
|
|
@@ -245,6 +306,7 @@ class RoundManager {
|
|
|
245
306
|
if (mapData) {
|
|
246
307
|
this._currentMapData = mapData;
|
|
247
308
|
this._scaledMapData = scaleMapData(mapData);
|
|
309
|
+
this._mapOverride = mapData;
|
|
248
310
|
}
|
|
249
311
|
}
|
|
250
312
|
|
|
@@ -265,6 +327,18 @@ class RoundManager {
|
|
|
265
327
|
this._handoffCallback = null;
|
|
266
328
|
}
|
|
267
329
|
|
|
330
|
+
// плановая передача хоста в игре без migration.midRound (host-migration
|
|
331
|
+
// этап 8d): колбэк сработает один раз, когда стартует следующий раунд.
|
|
332
|
+
// Раунд не придерживается — бета всё равно начнёт его заново из мягкой
|
|
333
|
+
// точки, а сорвавшаяся передача не должна оставить комнату без раунда
|
|
334
|
+
onRoundBoundary(cb) {
|
|
335
|
+
this._boundaryCallback = cb;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
cancelRoundBoundary() {
|
|
339
|
+
this._boundaryCallback = null;
|
|
340
|
+
}
|
|
341
|
+
|
|
268
342
|
// запуск нового раунда
|
|
269
343
|
initiateNewRound() {
|
|
270
344
|
this._timerManager.stopRoundTimer();
|
|
@@ -282,6 +356,13 @@ class RoundManager {
|
|
|
282
356
|
|
|
283
357
|
this._startRound();
|
|
284
358
|
this._timerManager.startRoundTimer();
|
|
359
|
+
|
|
360
|
+
if (this._boundaryCallback) {
|
|
361
|
+
const cb = this._boundaryCallback;
|
|
362
|
+
|
|
363
|
+
this._boundaryCallback = null;
|
|
364
|
+
cb();
|
|
365
|
+
}
|
|
285
366
|
}
|
|
286
367
|
|
|
287
368
|
// начало раунда
|
|
@@ -862,10 +943,7 @@ class RoundManager {
|
|
|
862
943
|
if (result === 'Yes' && this._maps[mapName]) {
|
|
863
944
|
this._chat.pushSystem('VOTE_PASSED');
|
|
864
945
|
this._chat.pushSystem('MAP_NEXT', [mapName]);
|
|
865
|
-
this.
|
|
866
|
-
this._currentMap = mapName;
|
|
867
|
-
this.createMap();
|
|
868
|
-
});
|
|
946
|
+
this.scheduleMapChange(mapName);
|
|
869
947
|
} else {
|
|
870
948
|
this._chat.pushSystem('VOTE_FAILED');
|
|
871
949
|
}
|
|
@@ -890,10 +968,7 @@ class RoundManager {
|
|
|
890
968
|
if (resultingMapName && this._maps[resultingMapName]) {
|
|
891
969
|
this._chat.pushSystem('VOTE_PASSED');
|
|
892
970
|
this._chat.pushSystem('MAP_NEXT', [resultingMapName]);
|
|
893
|
-
this.
|
|
894
|
-
this._currentMap = resultingMapName;
|
|
895
|
-
this.createMap();
|
|
896
|
-
});
|
|
971
|
+
this.scheduleMapChange(resultingMapName);
|
|
897
972
|
} else {
|
|
898
973
|
// если никто не проголосовал, продлеваем время текущей карты
|
|
899
974
|
this._timerManager.stopMapTimer();
|
|
@@ -104,7 +104,10 @@ export default class Accolades {
|
|
|
104
104
|
return;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
if (
|
|
107
|
+
if (
|
|
108
|
+
this._lastRefreshAt !== null &&
|
|
109
|
+
now - this._lastRefreshAt < this._interval
|
|
110
|
+
) {
|
|
108
111
|
return;
|
|
109
112
|
}
|
|
110
113
|
|
|
@@ -172,6 +172,36 @@ class Panel {
|
|
|
172
172
|
return panelData;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
// значения панели участников для контрольной точки (host-migration
|
|
176
|
+
// этап 5): { [gameId]: values }. Несостоявшиеся изменения не переносятся —
|
|
177
|
+
// возобновлённый клиент получает панель целиком
|
|
178
|
+
serialize() {
|
|
179
|
+
return Object.fromEntries(
|
|
180
|
+
Object.entries(this._data).map(([gameId, user]) => [
|
|
181
|
+
gameId,
|
|
182
|
+
{ ...user.values },
|
|
183
|
+
]),
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// применяет значения из точки к уже заведённым (addUser) участникам;
|
|
188
|
+
// поля вне схемы панели отбрасываются
|
|
189
|
+
restore(state = {}) {
|
|
190
|
+
for (const [gameId, values] of Object.entries(state)) {
|
|
191
|
+
const user = this._data[gameId];
|
|
192
|
+
|
|
193
|
+
if (!user) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (const [param, value] of Object.entries(values ?? {})) {
|
|
198
|
+
if (Object.hasOwn(this._fields, param)) {
|
|
199
|
+
user.values[param] = value;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
175
205
|
// возвращает пустые данные (ключи без значений)
|
|
176
206
|
getEmptyPanel() {
|
|
177
207
|
return [`t:${this._lastSentRoundTime}`].concat(this._emptyPanel);
|