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
|
@@ -52,13 +52,38 @@ export default class HostController {
|
|
|
52
52
|
* @param {Function} [opts.onError] - сбой инициализации Worker'а
|
|
53
53
|
* (WASM/конфиг): комната не поднялась, нужно вернуть пользователя в лобби.
|
|
54
54
|
* @param {Function} [opts.onMapChange] - смена карты в комнате (голосование/
|
|
55
|
-
* таймер)
|
|
55
|
+
* таймер).
|
|
56
|
+
* @param {Function} [opts.onLobbyInfoChange] - смена строки карточки
|
|
57
|
+
* комнаты в лобби (string|null) — для актуализации info у мастера.
|
|
56
58
|
* @param {Object} [opts.diagnostics] - журнал клиентских ошибок
|
|
57
59
|
* (lib/diagnostics.js, plan/client-reports): туда уходят ошибки Worker'а.
|
|
60
|
+
* @param {Uint8Array} [opts.checkpoint] - сжатая контрольная точка
|
|
61
|
+
* (host-migration этап 5): Worker поднимает матч из неё на паузе, старт —
|
|
62
|
+
* startAfterRestore(). Буфер передаётся списком переноса — после
|
|
63
|
+
* конструктора он у вызывающего пуст.
|
|
64
|
+
* @param {number} [opts.seqFloor] - номер последнего кадра, который видели
|
|
65
|
+
* клиенты: восстановленный матч продолжит нумерацию дальше него.
|
|
66
|
+
* @param {boolean} [opts.preload] - прогрев преемника (host-migration этап
|
|
67
|
+
* 6): Worker импортирует плагин и компилирует wasm, матч не создаёт;
|
|
68
|
+
* готовность — onPreloaded.
|
|
69
|
+
* @param {Function} [opts.onPreloaded] - Worker прогрет ({ gameId,
|
|
70
|
+
* gameVersion, wasmCompiled }).
|
|
58
71
|
*/
|
|
59
72
|
constructor(
|
|
60
73
|
room,
|
|
61
|
-
{
|
|
74
|
+
{
|
|
75
|
+
workerFactory,
|
|
76
|
+
workerUrl,
|
|
77
|
+
onReady,
|
|
78
|
+
onError,
|
|
79
|
+
onMapChange,
|
|
80
|
+
onLobbyInfoChange,
|
|
81
|
+
diagnostics,
|
|
82
|
+
checkpoint = null,
|
|
83
|
+
seqFloor = 0,
|
|
84
|
+
preload = false,
|
|
85
|
+
onPreloaded,
|
|
86
|
+
} = {},
|
|
62
87
|
) {
|
|
63
88
|
this._room = room;
|
|
64
89
|
this._workerFactory = workerFactory;
|
|
@@ -69,21 +94,47 @@ export default class HostController {
|
|
|
69
94
|
this._onReady = onReady;
|
|
70
95
|
this._onError = onError;
|
|
71
96
|
this._onMapChange = onMapChange;
|
|
97
|
+
this._onLobbyInfoChange = onLobbyInfoChange;
|
|
98
|
+
this._onPreloaded = onPreloaded;
|
|
72
99
|
this._ready = false;
|
|
73
100
|
this._deliveries = new Map(); // socketId → { onMessage, onClose }
|
|
74
|
-
this._pendingConnects = []; //
|
|
101
|
+
this._pendingConnects = []; // connect-сообщения до готовности Worker'а
|
|
75
102
|
|
|
76
103
|
this._swap = null; // состояние эстафеты (Этап 5.2)
|
|
77
104
|
|
|
105
|
+
// ожидание границы раунда плановой передачей (host-migration этап 8d)
|
|
106
|
+
this._roundBoundaryCallback = null;
|
|
107
|
+
|
|
108
|
+
// подписчики готовых контрольных точек (host-migration этап 5)
|
|
109
|
+
this._checkpointListeners = new Set();
|
|
110
|
+
// период включённых периодических точек (null — выключены): новый Worker
|
|
111
|
+
// эстафеты получает его заново, иначе поток точек бете молча встал бы
|
|
112
|
+
this._checkpointIntervalMs = null;
|
|
113
|
+
|
|
114
|
+
// подписчики метрик здоровья хоста (host-migration этап 9a)
|
|
115
|
+
this._healthListeners = new Set();
|
|
116
|
+
|
|
78
117
|
// отладочные запросы в Worker (этап 6 плана plan/done/ai-debug):
|
|
79
118
|
// requestId → { resolve, reject }
|
|
80
119
|
this._debugRequests = new Map();
|
|
81
120
|
this._debugRequestId = 0;
|
|
82
121
|
|
|
122
|
+
// ожидание ответа Worker'а на shutdown (закрытие комнаты)
|
|
123
|
+
this._shutdownResolve = null;
|
|
124
|
+
this._shutdownPromise = null;
|
|
125
|
+
|
|
83
126
|
this._worker.onmessage = e => this._onWorkerMessage(e.data);
|
|
84
127
|
|
|
85
|
-
// старт авторитетной части в Worker'
|
|
86
|
-
|
|
128
|
+
// старт авторитетной части в Worker'е; контрольная точка — без копии
|
|
129
|
+
if (preload) {
|
|
130
|
+
this._worker.postMessage({ type: 'preload', room });
|
|
131
|
+
} else if (checkpoint) {
|
|
132
|
+
this._worker.postMessage({ type: 'init', room, checkpoint, seqFloor }, [
|
|
133
|
+
checkpoint.buffer,
|
|
134
|
+
]);
|
|
135
|
+
} else {
|
|
136
|
+
this._worker.postMessage({ type: 'init', room });
|
|
137
|
+
}
|
|
87
138
|
}
|
|
88
139
|
|
|
89
140
|
_createWorker(url) {
|
|
@@ -115,21 +166,28 @@ export default class HostController {
|
|
|
115
166
|
}
|
|
116
167
|
|
|
117
168
|
// регистрирует клиента и (при готовности) поднимает его соединение в Worker'е
|
|
118
|
-
|
|
169
|
+
// resume — переподключение гостя к матчу (host-migration этап 4): Worker
|
|
170
|
+
// ждёт RESUME_REQUEST вместо хендшейка. Флаг едет только когда поднят —
|
|
171
|
+
// сообщение остаётся тем же, что понимал Worker до этапа 4
|
|
172
|
+
open(socketId, { onMessage, onClose, resume = false }) {
|
|
119
173
|
this._deliveries.set(socketId, { onMessage, onClose });
|
|
120
174
|
|
|
175
|
+
const msg = resume
|
|
176
|
+
? { type: 'connect', socketId, resume: true }
|
|
177
|
+
: { type: 'connect', socketId };
|
|
178
|
+
|
|
121
179
|
if (!this._ready) {
|
|
122
|
-
this._pendingConnects.push(
|
|
180
|
+
this._pendingConnects.push(msg);
|
|
123
181
|
return;
|
|
124
182
|
}
|
|
125
183
|
|
|
126
184
|
// пауза эстафеты: connect доедет в новый Worker (или в старый при отмене)
|
|
127
185
|
if (this._swap?.paused) {
|
|
128
|
-
this._enqueueSwapMessage(
|
|
186
|
+
this._enqueueSwapMessage(msg);
|
|
129
187
|
return;
|
|
130
188
|
}
|
|
131
189
|
|
|
132
|
-
this._worker.postMessage(
|
|
190
|
+
this._worker.postMessage(msg);
|
|
133
191
|
}
|
|
134
192
|
|
|
135
193
|
// пересылает входящее сообщение клиента в Worker
|
|
@@ -158,15 +216,15 @@ export default class HostController {
|
|
|
158
216
|
this._worker.postMessage(msg);
|
|
159
217
|
}
|
|
160
218
|
|
|
161
|
-
// сообщает Worker'у
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
this._room.
|
|
168
|
-
this._room.
|
|
169
|
-
this._worker.postMessage({ type: '
|
|
219
|
+
// сообщает Worker'у roomId + секрет эпохи, подтверждённые мастером в
|
|
220
|
+
// host_registered — не известны при создании Worker'а (постится в него
|
|
221
|
+
// раньше ответа мастера). Сохраняются в _room, чтобы эстафета (swapWorker)
|
|
222
|
+
// тоже понесла их в новый Worker через 'init'
|
|
223
|
+
setRoom({ roomId, roomSecret, epoch }) {
|
|
224
|
+
this._room.roomId = roomId;
|
|
225
|
+
this._room.roomSecret = roomSecret;
|
|
226
|
+
this._room.epoch = epoch;
|
|
227
|
+
this._worker.postMessage({ type: 'set_room', roomId, roomSecret, epoch });
|
|
170
228
|
}
|
|
171
229
|
|
|
172
230
|
// передаёт обновлённый каталог карт мастера в Worker (Этап 5.1);
|
|
@@ -223,6 +281,180 @@ export default class HostController {
|
|
|
223
281
|
});
|
|
224
282
|
}
|
|
225
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Своп уже забрал состояние Worker'а (после prepare_handoff): снять его
|
|
286
|
+
* нельзя.
|
|
287
|
+
* @returns {boolean}
|
|
288
|
+
*/
|
|
289
|
+
get swapCarryingState() {
|
|
290
|
+
return this._swap?.paused === true;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Снять эстафету, ждущую границы раунда (плановая передача хоста
|
|
295
|
+
* важнее). Своп уже переносит состояние — снять нельзя.
|
|
296
|
+
* @returns {boolean} эстафета снята.
|
|
297
|
+
*/
|
|
298
|
+
cancelPendingSwap() {
|
|
299
|
+
if (!this._swap || this._swap.paused) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const { reject } = this._swap;
|
|
304
|
+
|
|
305
|
+
this._swap = null;
|
|
306
|
+
this._worker.postMessage({ type: 'cancel_handoff' });
|
|
307
|
+
reject(new Error('swap preempted'));
|
|
308
|
+
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ***** контрольные точки (host-migration этап 5) ***** //
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Периодические контрольные точки: Worker снимает их на границе кадра не
|
|
316
|
+
* чаще intervalMs и отдаёт подписчикам onCheckpoint.
|
|
317
|
+
* @param {number} intervalMs
|
|
318
|
+
*/
|
|
319
|
+
startCheckpoints(intervalMs) {
|
|
320
|
+
this._checkpointIntervalMs = intervalMs;
|
|
321
|
+
|
|
322
|
+
// пауза эстафеты: состояние доедет до нужного Worker'а в конце свопа
|
|
323
|
+
if (!this._swap?.paused) {
|
|
324
|
+
this._worker.postMessage({ type: 'checkpoint_start', intervalMs });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
stopCheckpoints() {
|
|
329
|
+
this._checkpointIntervalMs = null;
|
|
330
|
+
|
|
331
|
+
if (!this._swap?.paused) {
|
|
332
|
+
this._worker.postMessage({ type: 'checkpoint_stop' });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// текущее состояние периодических точек — Worker'у после эстафеты
|
|
337
|
+
_postCheckpointState(worker) {
|
|
338
|
+
worker.postMessage(
|
|
339
|
+
this._checkpointIntervalMs === null
|
|
340
|
+
? { type: 'checkpoint_stop' }
|
|
341
|
+
: {
|
|
342
|
+
type: 'checkpoint_start',
|
|
343
|
+
intervalMs: this._checkpointIntervalMs,
|
|
344
|
+
},
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Одна точка на ближайшей границе кадра.
|
|
350
|
+
* @param {Object} [options]
|
|
351
|
+
* @param {boolean} [options.final] - финальная (перед плановой передачей).
|
|
352
|
+
*/
|
|
353
|
+
requestCheckpoint({ final = false } = {}) {
|
|
354
|
+
const msg = { type: 'checkpoint_request', final };
|
|
355
|
+
|
|
356
|
+
if (this._swap?.paused) {
|
|
357
|
+
this._enqueueSwapMessage(msg);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
this._worker.postMessage(msg);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Подписка на готовые точки: cb({ checkpointId, seq, createdAt, final,
|
|
366
|
+
* mode, bytes }).
|
|
367
|
+
* @param {Function} cb
|
|
368
|
+
* @returns {Function} Отписка.
|
|
369
|
+
*/
|
|
370
|
+
onCheckpoint(cb) {
|
|
371
|
+
this._checkpointListeners.add(cb);
|
|
372
|
+
|
|
373
|
+
return () => this._checkpointListeners.delete(cb);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Подписка на метрики здоровья хоста (host-migration этап 9a): раз в ~1 с,
|
|
378
|
+
* пока матч идёт — cb({ tickRate, maxGapMs, lostMs, windowMs,
|
|
379
|
+
* peerRttMedian, peerCount }). Переживает эстафету Worker'ов.
|
|
380
|
+
* @param {Function} cb
|
|
381
|
+
* @returns {Function} Отписка.
|
|
382
|
+
*/
|
|
383
|
+
onHealth(cb) {
|
|
384
|
+
this._healthListeners.add(cb);
|
|
385
|
+
|
|
386
|
+
return () => this._healthListeners.delete(cb);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Плановая передача хоста (host-migration этап 8d): cb сработает на
|
|
391
|
+
* ближайшей границе раунда — у игры с migration.midRound сразу (решает
|
|
392
|
+
* Worker). Новый вызов заменяет прежний колбэк.
|
|
393
|
+
* @param {Function} cb
|
|
394
|
+
*/
|
|
395
|
+
awaitRoundBoundary(cb) {
|
|
396
|
+
this._roundBoundaryCallback = cb;
|
|
397
|
+
this._worker.postMessage({ type: 'round_boundary_wait' });
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
cancelRoundBoundary() {
|
|
401
|
+
if (this._roundBoundaryCallback) {
|
|
402
|
+
this._roundBoundaryCallback = null;
|
|
403
|
+
this._worker.postMessage({ type: 'round_boundary_cancel' });
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// заморозка матча: цикл и отсчёты встают с остатками
|
|
408
|
+
freeze() {
|
|
409
|
+
this._worker.postMessage({ type: 'freeze' });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
unfreeze() {
|
|
413
|
+
this._worker.postMessage({ type: 'unfreeze' });
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// матч поднят из контрольной точки — запустить цикл и таймеры.
|
|
417
|
+
// waitForResume — промоушен преемника: Worker ждёт возврата людей точки
|
|
418
|
+
// (не дольше resumeWaitMs) и заводит не вернувшимся ожидание; reason —
|
|
419
|
+
// причина передачи из promote (сообщение игрокам о смене хоста)
|
|
420
|
+
startAfterRestore({ waitForResume = false, reason = null } = {}) {
|
|
421
|
+
if (!waitForResume) {
|
|
422
|
+
this._worker.postMessage({ type: 'start_after_restore' });
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
this._worker.postMessage(
|
|
427
|
+
reason
|
|
428
|
+
? { type: 'start_after_restore', waitForResume: true, reason }
|
|
429
|
+
: { type: 'start_after_restore', waitForResume: true },
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Поднимает матч из контрольной точки в прогретом Worker'е (режим
|
|
435
|
+
* preload, host-migration этап 7.4). Колбэки — те же, что у конструктора:
|
|
436
|
+
* у прогрева они были свои.
|
|
437
|
+
* @param {Object} room - настройки комнаты (roomId, epoch, game, …).
|
|
438
|
+
* @param {Uint8Array} checkpoint - сжатая точка; буфер передаётся
|
|
439
|
+
* списком переноса.
|
|
440
|
+
* @param {Object} [opts] - { seqFloor, onReady, onError, onMapChange,
|
|
441
|
+
* onLobbyInfoChange }.
|
|
442
|
+
*/
|
|
443
|
+
initFromCheckpoint(
|
|
444
|
+
room,
|
|
445
|
+
checkpoint,
|
|
446
|
+
{ seqFloor = 0, onReady, onError, onMapChange, onLobbyInfoChange } = {},
|
|
447
|
+
) {
|
|
448
|
+
this._room = room;
|
|
449
|
+
this._onReady = onReady;
|
|
450
|
+
this._onError = onError;
|
|
451
|
+
this._onMapChange = onMapChange;
|
|
452
|
+
this._onLobbyInfoChange = onLobbyInfoChange;
|
|
453
|
+
this._worker.postMessage({ type: 'init', room, checkpoint, seqFloor }, [
|
|
454
|
+
checkpoint.buffer,
|
|
455
|
+
]);
|
|
456
|
+
}
|
|
457
|
+
|
|
226
458
|
/**
|
|
227
459
|
* Отладочный контур (этап 6 плана plan/done/ai-debug): начинает запись живого
|
|
228
460
|
* матча в формат сценария headless-runner'а.
|
|
@@ -266,7 +498,9 @@ export default class HostController {
|
|
|
266
498
|
const timer = setTimeout(() => {
|
|
267
499
|
this._debugRequests.delete(requestId);
|
|
268
500
|
reject(
|
|
269
|
-
new Error(
|
|
501
|
+
new Error(
|
|
502
|
+
`debug request '${action}' timed out after ${timeoutMs} ms`,
|
|
503
|
+
),
|
|
270
504
|
);
|
|
271
505
|
}, timeoutMs);
|
|
272
506
|
|
|
@@ -294,14 +528,68 @@ export default class HostController {
|
|
|
294
528
|
this._debugRequests.clear();
|
|
295
529
|
}
|
|
296
530
|
|
|
531
|
+
/**
|
|
532
|
+
* Корректное закрытие комнаты: Worker закрывает игры участников и пишет
|
|
533
|
+
* профили (HostGame.destroy). Разрешается по ответу Worker'а или по
|
|
534
|
+
* таймауту — Worker не гасится, это делает destroy(). Эстафета Worker'ов,
|
|
535
|
+
* ждущая границы раунда, сначала снимается (cancel_handoff); при
|
|
536
|
+
* эстафете, уже переносящей состояние, — сразу выполненный промис.
|
|
537
|
+
* @param {Object} [options]
|
|
538
|
+
* @param {number} [options.timeoutMs]
|
|
539
|
+
* @returns {Promise<void>}
|
|
540
|
+
*/
|
|
541
|
+
shutdown({ timeoutMs = 3000 } = {}) {
|
|
542
|
+
// эстафета уже переносит состояние: старый Worker записал очки перед
|
|
543
|
+
// отдачей меты (_flushBeforeHandoff) — закрывать нечего
|
|
544
|
+
if (this._swap?.paused) {
|
|
545
|
+
return Promise.resolve();
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// повторный вызов (второй клик «Leave server») — тот же промис: второй
|
|
549
|
+
// HostGame.destroy() поверх идущего повторил бы flush (двойной зачёт)
|
|
550
|
+
if (this._shutdownPromise) {
|
|
551
|
+
return this._shutdownPromise;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// эстафета ждёт границы раунда: ничего не переносила и очков не писала.
|
|
555
|
+
// Комната закрывается — эстафета больше не нужна, а очки — нужны
|
|
556
|
+
if (this._swap) {
|
|
557
|
+
this.cancelPendingSwap();
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
this._shutdownPromise = new Promise(resolve => {
|
|
561
|
+
// страховка поверх таймаута Worker'а: зависший Worker не ответит вовсе
|
|
562
|
+
const timer = setTimeout(
|
|
563
|
+
() => this._shutdownResolve?.(),
|
|
564
|
+
timeoutMs + 500,
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
this._shutdownResolve = () => {
|
|
568
|
+
clearTimeout(timer);
|
|
569
|
+
this._shutdownResolve = null;
|
|
570
|
+
resolve();
|
|
571
|
+
};
|
|
572
|
+
this._worker.postMessage({ type: 'shutdown', timeoutMs });
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
return this._shutdownPromise;
|
|
576
|
+
}
|
|
577
|
+
|
|
297
578
|
// останавливает Worker (закрытие комнаты)
|
|
298
579
|
destroy() {
|
|
580
|
+
this._shutdownResolve?.();
|
|
581
|
+
|
|
299
582
|
this._rejectDebugRequests('host destroyed');
|
|
300
583
|
|
|
301
584
|
if (this._swap) {
|
|
585
|
+
const { reject } = this._swap;
|
|
586
|
+
|
|
302
587
|
this._swap.next?.terminate();
|
|
303
588
|
this._clearSwapTimeout();
|
|
304
589
|
this._swap = null;
|
|
590
|
+
// своп ждёт HostRole.refreshWorker: без отказа его finally не
|
|
591
|
+
// выполнится, и флаг эстафеты залипнет до перезагрузки страницы
|
|
592
|
+
reject(new Error('host destroyed'));
|
|
305
593
|
}
|
|
306
594
|
|
|
307
595
|
this._worker.terminate();
|
|
@@ -324,8 +612,16 @@ export default class HostController {
|
|
|
324
612
|
|
|
325
613
|
// старый Worker достиг границы раунда и отдал состояние: поднять новый
|
|
326
614
|
_onHandoffState(state) {
|
|
615
|
+
// своп снят (cancelPendingSwap), а Worker успел отдать состояние раньше,
|
|
616
|
+
// чем получил cancel_handoff: его таймеры стоят — вернуть к игре. Если
|
|
617
|
+
// своп сняло закрытие комнаты, возвращать нечего: Worker уже в
|
|
618
|
+
// HostGame.destroy(). После destroy() сообщение ничего не сделает
|
|
327
619
|
if (!this._swap) {
|
|
328
|
-
|
|
620
|
+
if (!this._shutdownPromise) {
|
|
621
|
+
this._worker.postMessage({ type: 'resume' });
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return;
|
|
329
625
|
}
|
|
330
626
|
|
|
331
627
|
this._swap.paused = true;
|
|
@@ -359,6 +655,14 @@ export default class HostController {
|
|
|
359
655
|
_onNextWorkerMessage(msg) {
|
|
360
656
|
if (msg.type === 'ready') {
|
|
361
657
|
this._finishSwap();
|
|
658
|
+
|
|
659
|
+
// новый Worker своё начальное значение отдельным lobby_info не шлёт
|
|
660
|
+
// (считает его уже сообщённым), а оно могло измениться: новая версия
|
|
661
|
+
// игры включила lobbyInfo, или карта/текст модулей другие. Worker без
|
|
662
|
+
// поля (старше этапа 2 host-migration) карточку не трогает
|
|
663
|
+
if ('lobbyInfo' in msg) {
|
|
664
|
+
this._onLobbyInfoChange?.(msg.lobbyInfo ?? null);
|
|
665
|
+
}
|
|
362
666
|
} else if (msg.type === 'error') {
|
|
363
667
|
this._reportWorkerMessage(msg, 'error');
|
|
364
668
|
this._abortSwap(msg.message);
|
|
@@ -396,6 +700,11 @@ export default class HostController {
|
|
|
396
700
|
|
|
397
701
|
next.postMessage({ type: 'handoff_complete' });
|
|
398
702
|
|
|
703
|
+
// новый Worker не знает, что точки были включены
|
|
704
|
+
if (this._checkpointIntervalMs !== null) {
|
|
705
|
+
this._postCheckpointState(next);
|
|
706
|
+
}
|
|
707
|
+
|
|
399
708
|
this._worker.terminate();
|
|
400
709
|
this._worker = next;
|
|
401
710
|
this._worker.onmessage = e => this._onWorkerMessage(e.data);
|
|
@@ -418,6 +727,9 @@ export default class HostController {
|
|
|
418
727
|
this._worker.postMessage(msg);
|
|
419
728
|
}
|
|
420
729
|
|
|
730
|
+
// за паузу точки могли включить или выключить
|
|
731
|
+
this._postCheckpointState(this._worker);
|
|
732
|
+
|
|
421
733
|
this._swap = null;
|
|
422
734
|
|
|
423
735
|
reject(new Error(reason || 'worker swap failed'));
|
|
@@ -428,8 +740,8 @@ export default class HostController {
|
|
|
428
740
|
case 'ready':
|
|
429
741
|
this._ready = true;
|
|
430
742
|
|
|
431
|
-
for (const
|
|
432
|
-
this._worker.postMessage(
|
|
743
|
+
for (const msg of this._pendingConnects) {
|
|
744
|
+
this._worker.postMessage(msg);
|
|
433
745
|
}
|
|
434
746
|
|
|
435
747
|
this._pendingConnects.length = 0;
|
|
@@ -441,6 +753,10 @@ export default class HostController {
|
|
|
441
753
|
this._onError?.(msg);
|
|
442
754
|
break;
|
|
443
755
|
|
|
756
|
+
case 'preloaded':
|
|
757
|
+
this._onPreloaded?.(msg);
|
|
758
|
+
break;
|
|
759
|
+
|
|
444
760
|
case 'diagnostic':
|
|
445
761
|
this._reportWorkerMessage(msg, msg.kind);
|
|
446
762
|
break;
|
|
@@ -449,10 +765,45 @@ export default class HostController {
|
|
|
449
765
|
this._onMapChange?.(msg.mapName);
|
|
450
766
|
break;
|
|
451
767
|
|
|
768
|
+
case 'lobby_info':
|
|
769
|
+
this._onLobbyInfoChange?.(msg.info ?? null);
|
|
770
|
+
break;
|
|
771
|
+
|
|
452
772
|
case 'handoff_state':
|
|
453
773
|
this._onHandoffState(msg.state);
|
|
454
774
|
break;
|
|
455
775
|
|
|
776
|
+
case 'round_boundary': {
|
|
777
|
+
const cb = this._roundBoundaryCallback;
|
|
778
|
+
|
|
779
|
+
this._roundBoundaryCallback = null;
|
|
780
|
+
cb?.();
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
case 'checkpoint': {
|
|
785
|
+
const checkpoint = { ...msg };
|
|
786
|
+
|
|
787
|
+
delete checkpoint.type;
|
|
788
|
+
|
|
789
|
+
for (const listener of this._checkpointListeners) {
|
|
790
|
+
listener(checkpoint);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
case 'health':
|
|
797
|
+
for (const listener of this._healthListeners) {
|
|
798
|
+
listener(msg.health);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
break;
|
|
802
|
+
|
|
803
|
+
case 'shutdown_done':
|
|
804
|
+
this._shutdownResolve?.();
|
|
805
|
+
break;
|
|
806
|
+
|
|
456
807
|
case 'debug_result': {
|
|
457
808
|
const pending = this._debugRequests.get(msg.requestId);
|
|
458
809
|
|
|
@@ -470,7 +821,9 @@ export default class HostController {
|
|
|
470
821
|
}
|
|
471
822
|
|
|
472
823
|
case 'to_client':
|
|
473
|
-
this._deliveries
|
|
824
|
+
this._deliveries
|
|
825
|
+
.get(msg.socketId)
|
|
826
|
+
?.onMessage(msg.payload, msg.reliable);
|
|
474
827
|
break;
|
|
475
828
|
|
|
476
829
|
case 'close_client':
|