vimp-engine 0.41.0 → 0.42.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/package.json +1 -1
- package/src/client/components/controller/RoomMenu.js +38 -10
- package/src/client/components/model/RoomMenu.js +42 -57
- package/src/client/components/view/RoomMenu.js +78 -24
- package/src/client/lib/autoRejoin.js +161 -0
- package/src/client/lib/hostUnloadGuard.js +40 -15
- package/src/client/lib/roomLink.js +32 -0
- package/src/client/main.js +116 -36
- package/src/client/network/HostConnectionManager.js +27 -1
- package/src/client/network/HostController.js +1 -30
- package/src/client/network/HostHealthPolicy.js +14 -59
- package/src/client/network/PlannedHandoff.js +15 -100
- package/src/client/network/SessionSupervisor.js +58 -9
- package/src/client/network/SignalingClient.js +1 -1
- package/src/client/network/WebRtcManager.js +38 -3
- package/src/client/session/GuestSession.js +42 -7
- package/src/client/session/HandoffFlow.js +26 -51
- package/src/client/session/HostRole.js +8 -6
- package/src/client/session/PromotionFlow.js +28 -1
- package/src/client/session/RouteBoot.js +28 -9
- package/src/client/style.css +31 -16
- package/src/client/views/includes/roomMenu.pug +7 -8
- package/src/config/chatMessages.js +1 -1
- package/src/config/lobby.js +22 -23
- package/src/devtools/ScenarioRunner.js +1 -0
- package/src/devtools/invariants.js +24 -2
- package/src/devtools/report.js +2 -2
- package/src/host/HostGame.js +14 -26
- package/src/host/PortMachine.js +34 -17
- package/src/host/host.worker.js +1 -13
- package/src/host/meta/core/RoundManager.js +0 -21
- package/src/host/meta/modules/chat/systemMessages.js +1 -1
- package/src/lib/capabilities.js +2 -4
- package/src/lib/gameConfigView.js +0 -6
package/src/devtools/report.js
CHANGED
|
@@ -103,8 +103,8 @@ export function formatMarkdown(report) {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
// смены хоста через контрольную точку: 'soft'
|
|
107
|
-
//
|
|
106
|
+
// смены хоста через контрольную точку: 'soft' (дамп ядра не снялся) —
|
|
107
|
+
// первое, что автор должен увидеть
|
|
108
108
|
if (report.checkpointRestores?.length) {
|
|
109
109
|
lines.push('');
|
|
110
110
|
lines.push('## Host checkpoints');
|
package/src/host/HostGame.js
CHANGED
|
@@ -156,9 +156,6 @@ export default class HostGame {
|
|
|
156
156
|
this._isDevMode = data.isDevMode || false;
|
|
157
157
|
this._seed = seed;
|
|
158
158
|
|
|
159
|
-
// контрольная точка (host-migration этап 5): игра гарантирует, что дамп
|
|
160
|
-
// ядра и хуки модулей переносят всё её состояние посреди раунда
|
|
161
|
-
this._migrationMidRound = data.migration?.midRound === true;
|
|
162
159
|
this._handoffFlushTimeoutMs = data.handoffFlushTimeoutMs ?? 3000;
|
|
163
160
|
// промоушен преемника (host-migration этап 7.4): сколько восстановленный
|
|
164
161
|
// матч ждёт возврата людей точки перед стартом
|
|
@@ -377,6 +374,9 @@ export default class HostGame {
|
|
|
377
374
|
this._lastCheckpointAt = 0;
|
|
378
375
|
this._checkpointRequest = null; // { final } — точка на ближайшем кадре
|
|
379
376
|
this._checkpointCounter = 0;
|
|
377
|
+
// дамп упал на прошлой точке: повторный сбой не спамит консоль дважды в
|
|
378
|
+
// секунду, а успех снимает флаг — следующий сбой снова виден
|
|
379
|
+
this._dumpFailing = false;
|
|
380
380
|
// матч поднят из точки и ждёт startAfterRestore: цикл и таймеры стоят
|
|
381
381
|
this._restorePending = false;
|
|
382
382
|
this._restoreMode = null; // 'midRound' | 'soft'
|
|
@@ -931,23 +931,6 @@ export default class HostGame {
|
|
|
931
931
|
this._roundManager.cancelHandoff();
|
|
932
932
|
}
|
|
933
933
|
|
|
934
|
-
// плановая передача хоста (host-migration этап 8d): игра без
|
|
935
|
-
// migration.midRound переносит только мету — передача посреди раунда
|
|
936
|
-
// отняла бы его у всех, поэтому она ждёт следующего раунда. Игра с
|
|
937
|
-
// midRound продолжает у преемника с того же тика — ждать нечего
|
|
938
|
-
awaitRoundBoundary(cb) {
|
|
939
|
-
if (this._migrationMidRound) {
|
|
940
|
-
cb();
|
|
941
|
-
return;
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
this._roundManager.onRoundBoundary(cb);
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
cancelRoundBoundary() {
|
|
948
|
-
this._roundManager.cancelRoundBoundary();
|
|
949
|
-
}
|
|
950
|
-
|
|
951
934
|
// финальная синхронизация профилей, но не дольше handoffFlushTimeoutMs:
|
|
952
935
|
// зависший auth-сервис не должен держать эстафету — неотправленное едет
|
|
953
936
|
// в состоянии (playerData) и уйдёт из нового Worker'а
|
|
@@ -1211,9 +1194,9 @@ export default class HostGame {
|
|
|
1211
1194
|
|
|
1212
1195
|
/**
|
|
1213
1196
|
* Собирает контрольную точку: мета (формат HANDOFF_VERSION 4) и дамп
|
|
1214
|
-
* ядра.
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1197
|
+
* ядра. Сетевая точка ('checkpoint') всегда пробует дамп; не снялся —
|
|
1198
|
+
* режим 'soft' (восстановление начнёт раунд заново). Эстафета Worker'ов
|
|
1199
|
+
* ('boundary') стоит на границе раунда и ядро не несёт.
|
|
1217
1200
|
* @param {'checkpoint'|'boundary'} kind
|
|
1218
1201
|
* @returns {{meta: Object, core: Uint8Array|null}}
|
|
1219
1202
|
*/
|
|
@@ -1221,14 +1204,19 @@ export default class HostGame {
|
|
|
1221
1204
|
let core = null;
|
|
1222
1205
|
let plugin = null;
|
|
1223
1206
|
|
|
1224
|
-
if (kind === 'checkpoint'
|
|
1207
|
+
if (kind === 'checkpoint') {
|
|
1225
1208
|
try {
|
|
1226
1209
|
plugin = this._serializeModules();
|
|
1227
1210
|
core = this._game.serializeState();
|
|
1211
|
+
this._dumpFailing = false;
|
|
1228
1212
|
} catch (e) {
|
|
1229
1213
|
// точка остаётся полезной и без ядра: аварийный преемник начнёт
|
|
1230
1214
|
// раунд заново, а не потеряет комнату
|
|
1231
|
-
|
|
1215
|
+
if (!this._dumpFailing) {
|
|
1216
|
+
console.warn(`[checkpoint] mid-round state skipped: ${e.message}`);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
this._dumpFailing = true;
|
|
1232
1220
|
core = null;
|
|
1233
1221
|
plugin = null;
|
|
1234
1222
|
}
|
|
@@ -2075,7 +2063,7 @@ export default class HostGame {
|
|
|
2075
2063
|
// транспорт участника оборвался без LEAVE: место держится до возврата.
|
|
2076
2064
|
// Слот занят (isFull его считает), актор остаётся в мире, но все его
|
|
2077
2065
|
// команды отпущены; RTT- и idle-кики его не трогают. false — держать
|
|
2078
|
-
// нечего (
|
|
2066
|
+
// нечего (нет ключа места, хост-игрок, уже отсоединён) — снимать сразу
|
|
2079
2067
|
detachUser(gameId) {
|
|
2080
2068
|
const user = this._participants.get(gameId);
|
|
2081
2069
|
|
package/src/host/PortMachine.js
CHANGED
|
@@ -145,6 +145,8 @@ export default class PortMachine {
|
|
|
145
145
|
gameId: undefined,
|
|
146
146
|
enabled: new Array(PORT_COUNT).fill(false),
|
|
147
147
|
refreshing: false,
|
|
148
|
+
// соединение входило в матч (первый кадр или RESUME)
|
|
149
|
+
entered: false,
|
|
148
150
|
};
|
|
149
151
|
|
|
150
152
|
this._clients.set(socketId, state);
|
|
@@ -160,6 +162,8 @@ export default class PortMachine {
|
|
|
160
162
|
gameId: undefined,
|
|
161
163
|
enabled: new Array(PORT_COUNT).fill(false),
|
|
162
164
|
refreshing: false,
|
|
165
|
+
// соединение входило в матч (первый кадр или RESUME)
|
|
166
|
+
entered: false,
|
|
163
167
|
resumeTimer: null,
|
|
164
168
|
};
|
|
165
169
|
|
|
@@ -218,6 +222,7 @@ export default class PortMachine {
|
|
|
218
222
|
gameId,
|
|
219
223
|
enabled: new Array(PORT_COUNT).fill(false),
|
|
220
224
|
refreshing: false,
|
|
225
|
+
entered: true,
|
|
221
226
|
};
|
|
222
227
|
|
|
223
228
|
state.methods = this._buildPortMethods(socketId, state);
|
|
@@ -281,8 +286,14 @@ export default class PortMachine {
|
|
|
281
286
|
}
|
|
282
287
|
|
|
283
288
|
// обрыв без LEAVE (host-migration этап 4): место держится resumeGraceMs,
|
|
284
|
-
// если
|
|
285
|
-
|
|
289
|
+
// если соединение уже входило в матч — только из матча клиент умеет
|
|
290
|
+
// вернуться (SessionSupervisor). Ключ у грузящего карту есть ради точки
|
|
291
|
+
// смены хоста, но его обрыв до первого кадра снимает место сразу
|
|
292
|
+
if (
|
|
293
|
+
this._resumeGraceMs > 0 &&
|
|
294
|
+
state.entered &&
|
|
295
|
+
this._host.detachUser(state.gameId)
|
|
296
|
+
) {
|
|
286
297
|
this._startGrace(state.gameId);
|
|
287
298
|
return;
|
|
288
299
|
}
|
|
@@ -389,6 +400,7 @@ export default class PortMachine {
|
|
|
389
400
|
this._stopGrace(gameId);
|
|
390
401
|
|
|
391
402
|
state.gameId = gameId;
|
|
403
|
+
state.entered = true;
|
|
392
404
|
state.enabled[PC_RESUME_REQUEST] = false;
|
|
393
405
|
this._enableInGame(state);
|
|
394
406
|
|
|
@@ -397,6 +409,23 @@ export default class PortMachine {
|
|
|
397
409
|
.catch(() => reject('auth'));
|
|
398
410
|
}
|
|
399
411
|
|
|
412
|
+
// участник вошёл — с этого момента ему есть куда вернуться: секрет места
|
|
413
|
+
// (SESSION_DATA). Выдаётся при принятии авторизации, а не на первом
|
|
414
|
+
// кадре: смена хоста посреди загрузки карты не должна стоить места
|
|
415
|
+
_issueSession(socketId, gameId) {
|
|
416
|
+
if (this._resumeGraceMs <= 0 || !this._clients.has(socketId)) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const resumeKey = this._host.issueResumeKey(gameId);
|
|
421
|
+
|
|
422
|
+
// gameId — клиенту для RESUME_REQUEST: наблюдатель свой id больше
|
|
423
|
+
// ниоткуда не узнаёт
|
|
424
|
+
if (resumeKey) {
|
|
425
|
+
this._socketManager.sendSessionData(socketId, { resumeKey, gameId });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
400
429
|
// TOKEN_REFRESH { token }: токен принимается, только если он подписан
|
|
401
430
|
// auth-сервисом и выдан на тот же ник, под которым участник вошёл
|
|
402
431
|
_refreshToken(socketId, state, data) {
|
|
@@ -526,6 +555,7 @@ export default class PortMachine {
|
|
|
526
555
|
|
|
527
556
|
host.createUser({ ...data, name }, socketId, createdId => {
|
|
528
557
|
state.gameId = createdId;
|
|
558
|
+
this._issueSession(socketId, createdId);
|
|
529
559
|
});
|
|
530
560
|
|
|
531
561
|
socketManager.sendTechInform(socketId, 'loading');
|
|
@@ -549,23 +579,10 @@ export default class PortMachine {
|
|
|
549
579
|
// 3: map ready
|
|
550
580
|
() => host.mapReady(state.gameId),
|
|
551
581
|
|
|
552
|
-
// 4: first shot ready
|
|
553
|
-
// есть куда вернуться после обрыва: секрет места (SESSION_DATA)
|
|
582
|
+
// 4: first shot ready: участник в матче — обрыв держит место
|
|
554
583
|
() => {
|
|
584
|
+
state.entered = true;
|
|
555
585
|
host.firstShotReady(state.gameId);
|
|
556
|
-
|
|
557
|
-
if (this._resumeGraceMs > 0) {
|
|
558
|
-
const resumeKey = host.issueResumeKey(state.gameId);
|
|
559
|
-
|
|
560
|
-
// gameId — клиенту для RESUME_REQUEST: наблюдатель свой id
|
|
561
|
-
// больше ниоткуда не узнаёт
|
|
562
|
-
if (resumeKey) {
|
|
563
|
-
socketManager.sendSessionData(socketId, {
|
|
564
|
-
resumeKey,
|
|
565
|
-
gameId: state.gameId,
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
586
|
},
|
|
570
587
|
|
|
571
588
|
// 5: keys data ('seq:action:name'; указатель — 'seq:aim:x:y:flags')
|
package/src/host/host.worker.js
CHANGED
|
@@ -402,18 +402,6 @@ self.onmessage = async event => {
|
|
|
402
402
|
}
|
|
403
403
|
break;
|
|
404
404
|
|
|
405
|
-
// плановая передача хоста (этап 8d): дождаться границы раунда (у игры
|
|
406
|
-
// с migration.midRound — ответ сразу)
|
|
407
|
-
case 'round_boundary_wait':
|
|
408
|
-
host?.awaitRoundBoundary(() =>
|
|
409
|
-
self.postMessage({ type: 'round_boundary' }),
|
|
410
|
-
);
|
|
411
|
-
break;
|
|
412
|
-
|
|
413
|
-
case 'round_boundary_cancel':
|
|
414
|
-
host?.cancelRoundBoundary();
|
|
415
|
-
break;
|
|
416
|
-
|
|
417
405
|
case 'freeze':
|
|
418
406
|
host?.freeze();
|
|
419
407
|
break;
|
|
@@ -422,7 +410,7 @@ self.onmessage = async event => {
|
|
|
422
410
|
host?.unfreeze();
|
|
423
411
|
break;
|
|
424
412
|
|
|
425
|
-
// хост закрывает комнату (
|
|
413
|
+
// хост закрывает комнату (Exit последнего человека): закрыть
|
|
426
414
|
// игры участников и дождаться записи профилей — не дольше timeoutMs
|
|
427
415
|
case 'shutdown': {
|
|
428
416
|
const timeoutMs =
|
|
@@ -84,8 +84,6 @@ class RoundManager {
|
|
|
84
84
|
|
|
85
85
|
// эстафета Worker'ов (Этап 5.2): колбэк переноса на границе раунда
|
|
86
86
|
this._handoffCallback = null;
|
|
87
|
-
// плановая передача хоста ждёт границы раунда (host-migration этап 8d)
|
|
88
|
-
this._boundaryCallback = null;
|
|
89
87
|
}
|
|
90
88
|
|
|
91
89
|
get currentMap() {
|
|
@@ -325,18 +323,6 @@ class RoundManager {
|
|
|
325
323
|
this._handoffCallback = null;
|
|
326
324
|
}
|
|
327
325
|
|
|
328
|
-
// плановая передача хоста в игре без migration.midRound (host-migration
|
|
329
|
-
// этап 8d): колбэк сработает один раз, когда стартует следующий раунд.
|
|
330
|
-
// Раунд не придерживается — бета всё равно начнёт его заново из мягкой
|
|
331
|
-
// точки, а сорвавшаяся передача не должна оставить комнату без раунда
|
|
332
|
-
onRoundBoundary(cb) {
|
|
333
|
-
this._boundaryCallback = cb;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
cancelRoundBoundary() {
|
|
337
|
-
this._boundaryCallback = null;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
326
|
// запуск нового раунда
|
|
341
327
|
initiateNewRound() {
|
|
342
328
|
this._timerManager.stopRoundTimer();
|
|
@@ -354,13 +340,6 @@ class RoundManager {
|
|
|
354
340
|
|
|
355
341
|
this._startRound();
|
|
356
342
|
this._timerManager.startRoundTimer();
|
|
357
|
-
|
|
358
|
-
if (this._boundaryCallback) {
|
|
359
|
-
const cb = this._boundaryCallback;
|
|
360
|
-
|
|
361
|
-
this._boundaryCallback = null;
|
|
362
|
-
cb();
|
|
363
|
-
}
|
|
364
343
|
}
|
|
365
344
|
|
|
366
345
|
// начало раунда
|
|
@@ -30,7 +30,7 @@ export const ENGINE_MESSAGE_CODES = Object.freeze({
|
|
|
30
30
|
// голосование «Change host» (host-migration этап 10): ведёт мастер,
|
|
31
31
|
// сообщения добавляет сам клиент, по проводу они не ходят
|
|
32
32
|
HOST_VOTE_USAGE: 'v:6', // Usage: /changehost
|
|
33
|
-
// You are the host —
|
|
33
|
+
// You are the host — the room hands the host role over automatically when needed
|
|
34
34
|
HOST_VOTE_IS_HOST: 'v:7',
|
|
35
35
|
HOST_VOTE_OFFLINE: 'v:8', // No connection to the master server
|
|
36
36
|
HOST_VOTE_RECENT: 'v:9', // A host vote was held recently
|
package/src/lib/capabilities.js
CHANGED
|
@@ -46,10 +46,8 @@ export const ENGINE_CAPABILITIES = createRegistry('engine-capabilities', [
|
|
|
46
46
|
// сервис пула `diagnostics` (журнал клиентских ошибок);
|
|
47
47
|
// опционален — игра не пишет его в `requires`
|
|
48
48
|
{ value: 'diagnostics', since: '0.35.0' },
|
|
49
|
-
// контрольная точка хоста посреди раунда (host-migration этап 5):
|
|
50
|
-
//
|
|
51
|
-
// Писать в `requires` не нужно: старый движок флаг просто игнорирует, и
|
|
52
|
-
// игра на нём мигрирует мягко
|
|
49
|
+
// контрольная точка хоста посреди раунда (host-migration этап 5): хуки
|
|
50
|
+
// модулей serializeState/restoreState. Писать в `requires` не нужно
|
|
53
51
|
{ value: 'host.migration', since: '0.36.0' },
|
|
54
52
|
// голосовая связь (gameConfig.voice, порты VOICE_*, сервис пула `voice`,
|
|
55
53
|
// хуки `hooks.voiceSounds`/`hooks.voiceFilter`); опциональна — игра не
|
|
@@ -74,12 +74,6 @@ const FIELDS = {
|
|
|
74
74
|
// сливает конфиг поверхностно, и игра, задавшая `timers`, потеряла бы
|
|
75
75
|
// остальные таймеры движка
|
|
76
76
|
voteHoldTime: { default: 0 },
|
|
77
|
-
|
|
78
|
-
// смена хоста посреди раунда (host-migration этап 5): true — игра
|
|
79
|
-
// гарантирует, что GameSim::serialize/deserialize и хуки модулей
|
|
80
|
-
// serializeState/restoreState переносят всё её состояние. Без поля —
|
|
81
|
-
// мягкий режим: мета переносится, раунд начинается заново
|
|
82
|
-
'migration.midRound': { default: false },
|
|
83
77
|
};
|
|
84
78
|
|
|
85
79
|
// Все пути gameConfig, которые движок читает: объявленные игрой поля плюс
|