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,179 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildHostCaps,
|
|
3
|
+
canHostIn,
|
|
4
|
+
tokenAllowsHosting,
|
|
5
|
+
} from '../lib/hostCaps.js';
|
|
6
|
+
import JoinRetry from '../lib/JoinRetry.js';
|
|
7
|
+
|
|
8
|
+
// Членство вкладки в комнате на мастере (лобби-режим): join_room, его повтор
|
|
9
|
+
// после рестарта мастера, возможности вкладки для выбора беты (caps,
|
|
10
|
+
// master/successor.js) и их пересылка, когда они меняются.
|
|
11
|
+
//
|
|
12
|
+
// Без DOM: сигналинг, токен, роль и таймеры инъектирует сборщик
|
|
13
|
+
// (client/main.js).
|
|
14
|
+
|
|
15
|
+
export default class Membership {
|
|
16
|
+
/**
|
|
17
|
+
* @param {Object} deps
|
|
18
|
+
* @param {Object} deps.signaling - SignalingClient.
|
|
19
|
+
* @param {Object} deps.ctx - client/session/roomContext.js.
|
|
20
|
+
* @param {string} deps.memberId - id вкладки на мастере.
|
|
21
|
+
* @param {Function} deps.getToken - () → identity-токен.
|
|
22
|
+
* @param {Function} deps.getTokenExpiresAt - () → мс эпохи | null.
|
|
23
|
+
* @param {Function} deps.isHost - () → вкладка держит Worker комнаты.
|
|
24
|
+
* @param {Object} deps.config - lobby-конфиг (migration, session).
|
|
25
|
+
* @param {Function} [deps.canHost] - () → может ли вкладка хостить.
|
|
26
|
+
* @param {Function} [deps.now]
|
|
27
|
+
* @param {Object} [deps.timers] - { setTimeout, clearTimeout }.
|
|
28
|
+
*/
|
|
29
|
+
constructor({
|
|
30
|
+
signaling,
|
|
31
|
+
ctx,
|
|
32
|
+
memberId,
|
|
33
|
+
getToken,
|
|
34
|
+
getTokenExpiresAt,
|
|
35
|
+
isHost,
|
|
36
|
+
config,
|
|
37
|
+
canHost = () => canHostIn(),
|
|
38
|
+
now = () => Date.now(),
|
|
39
|
+
timers = globalThis,
|
|
40
|
+
}) {
|
|
41
|
+
this._signaling = signaling;
|
|
42
|
+
this._ctx = ctx;
|
|
43
|
+
this._memberId = memberId;
|
|
44
|
+
this._getToken = getToken;
|
|
45
|
+
this._getTokenExpiresAt = getTokenExpiresAt;
|
|
46
|
+
this._isHost = isHost;
|
|
47
|
+
this._config = config;
|
|
48
|
+
this._canHost = canHost;
|
|
49
|
+
this._now = now;
|
|
50
|
+
this._timers = timers;
|
|
51
|
+
|
|
52
|
+
// проба модульного Worker'а не бесплатна — считается один раз, лениво
|
|
53
|
+
this._canHostCached = null;
|
|
54
|
+
// тип своего ICE-кандидата в выбранной паре с хостом комнаты
|
|
55
|
+
this._iceType = null;
|
|
56
|
+
// средний FPS рендера гостя за интервал отчёта (этап 9c); null — неизвестен
|
|
57
|
+
this._fps = null;
|
|
58
|
+
// момент, когда вход станет слишком коротким для роли хоста: caps
|
|
59
|
+
// пересылаются мастеру, и он пересматривает бету
|
|
60
|
+
this._tokenCapsTimer = null;
|
|
61
|
+
|
|
62
|
+
// повтор join_room, пока хост не вернул комнату рестартовавшему мастеру
|
|
63
|
+
this._joinRetry = new JoinRetry({
|
|
64
|
+
send: () => this.sendJoinRoom(),
|
|
65
|
+
windowMs: config.session.joinRetryWindowMs,
|
|
66
|
+
timers,
|
|
67
|
+
now,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get memberId() {
|
|
72
|
+
return this._memberId;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// членство гостя в комнате на мастере (вход, реконнект сигналинга, повтор)
|
|
76
|
+
sendJoinRoom() {
|
|
77
|
+
this._ctx.memberJoined = true;
|
|
78
|
+
this.armTokenCapsTimer();
|
|
79
|
+
this._signaling.joinRoom({
|
|
80
|
+
roomId: this._ctx.roomId,
|
|
81
|
+
memberId: this._memberId,
|
|
82
|
+
token: this._getToken(),
|
|
83
|
+
caps: this.caps(),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// хватит ли срока входа на роль хоста: register_host/reclaim_host
|
|
88
|
+
// предъявляют токен мастеру посреди матча, а продления нет
|
|
89
|
+
tokenAllowsHostRole() {
|
|
90
|
+
return tokenAllowsHosting(
|
|
91
|
+
this._getTokenExpiresAt(),
|
|
92
|
+
this._config.migration.minTokenLifetimeMs,
|
|
93
|
+
this._now(),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// возможности вкладки для мастера (выбор беты, master/successor.js); у
|
|
98
|
+
// хоста iceType не про него — он сам конец всех пар
|
|
99
|
+
caps() {
|
|
100
|
+
this._canHostCached ??= this._canHost();
|
|
101
|
+
|
|
102
|
+
const isHost = this._isHost();
|
|
103
|
+
|
|
104
|
+
return buildHostCaps({
|
|
105
|
+
canHost: this._canHostCached && this.tokenAllowsHostRole(),
|
|
106
|
+
iceType: isHost ? null : this._iceType,
|
|
107
|
+
// средний FPS рендера гостя (этап 9c): мастер не назначает бетой
|
|
108
|
+
// слабую вкладку
|
|
109
|
+
fps: isHost ? null : this._fps,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// возможности изменились (вкладка спрятана, сменился тип кандидата) —
|
|
114
|
+
// мастер пересматривает бету
|
|
115
|
+
sendUpdate() {
|
|
116
|
+
if (this._ctx.roomId) {
|
|
117
|
+
this._signaling.memberUpdate(this._ctx.roomId, this.caps());
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// тип ICE-кандидата — мастеру: по нему он выбирает бету (этап 6)
|
|
122
|
+
setIceType(type) {
|
|
123
|
+
this._iceType = type;
|
|
124
|
+
this.sendUpdate();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// свежий средний FPS гостя мастеру (этап 9c); хост его не шлёт
|
|
128
|
+
setFps(fps) {
|
|
129
|
+
this._fps = fps;
|
|
130
|
+
|
|
131
|
+
if (!this._isHost()) {
|
|
132
|
+
this.sendUpdate();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// canHost гаснет по времени, а не по событию — без таймера мастер держал бы
|
|
137
|
+
// бетой вкладку, которая откажется от промоушена
|
|
138
|
+
armTokenCapsTimer() {
|
|
139
|
+
this.clearTokenCapsTimer();
|
|
140
|
+
|
|
141
|
+
const expiresAt = this._getTokenExpiresAt();
|
|
142
|
+
|
|
143
|
+
if (!this._ctx.roomId || expiresAt === null) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const delay =
|
|
148
|
+
expiresAt - this._config.migration.minTokenLifetimeMs - this._now();
|
|
149
|
+
|
|
150
|
+
if (delay > 0) {
|
|
151
|
+
this._tokenCapsTimer = this._timers.setTimeout(() => {
|
|
152
|
+
this._tokenCapsTimer = null;
|
|
153
|
+
this.sendUpdate();
|
|
154
|
+
}, delay);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
clearTokenCapsTimer() {
|
|
159
|
+
this._timers.clearTimeout(this._tokenCapsTimer);
|
|
160
|
+
this._tokenCapsTimer = null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// unknownRoom на join_room: мастер, возможно, ещё ждёт хоста
|
|
164
|
+
scheduleJoinRetry() {
|
|
165
|
+
this._joinRetry.schedule();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// мастер принял членство (или повторять бессмысленно)
|
|
169
|
+
stopJoinRetry() {
|
|
170
|
+
this._joinRetry.stop();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// членство больше не объявляется: уход из комнаты или роль хоста (хост —
|
|
174
|
+
// участник своей комнаты через register_host, не join_room)
|
|
175
|
+
forget() {
|
|
176
|
+
this._ctx.memberJoined = false;
|
|
177
|
+
this._joinRetry.stop();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { buildSystemMessage } from '../../host/meta/modules/chat/systemMessages.js';
|
|
2
|
+
import { formatRoomLink } from '../lib/roomLink.js';
|
|
3
|
+
import LoopbackTransport from '../network/LoopbackTransport.js';
|
|
4
|
+
import Promotion, { savePendingPromotion } from '../network/Promotion.js';
|
|
5
|
+
|
|
6
|
+
// Промоушен преемника (host-migration этап 7.4) и разжалование бывшего хоста
|
|
7
|
+
// (этап 7.5): promote → матч из контрольной точки (или холодный перезапуск
|
|
8
|
+
// перезагрузкой) → регистрация комнаты → возврат своего игрока; отмена,
|
|
9
|
+
// отказ мастера, host_revoked.
|
|
10
|
+
//
|
|
11
|
+
// Без DOM: сигналинг, роли, супервизор, UI-действия и фабрики инъектирует
|
|
12
|
+
// сборщик (client/main.js).
|
|
13
|
+
|
|
14
|
+
export default class PromotionFlow {
|
|
15
|
+
/**
|
|
16
|
+
* @param {Object} deps
|
|
17
|
+
* @param {Object} deps.signaling - SignalingClient.
|
|
18
|
+
* @param {Object} deps.ctx - client/session/roomContext.js.
|
|
19
|
+
* @param {Object} deps.hostRole - client/session/HostRole.js.
|
|
20
|
+
* @param {Object} deps.standby - client/session/StandbyRole.js.
|
|
21
|
+
* @param {Object} deps.membership - client/session/Membership.js.
|
|
22
|
+
* @param {Function} deps.getHandoff - () → HandoffFlow.
|
|
23
|
+
* @param {Function} deps.getGuest - () → GuestSession.
|
|
24
|
+
* @param {Function} deps.getSupervisor - () → SessionSupervisor | null.
|
|
25
|
+
* @param {Function} deps.getActiveGameId - () → id активной игры | undefined.
|
|
26
|
+
* @param {Function} deps.getToken - () → identity-токен.
|
|
27
|
+
* @param {Function} deps.prepareRoom - prepareHostRoom (hostRoomPrep.js).
|
|
28
|
+
* @param {Object} deps.config - lobby-конфиг (migration, create).
|
|
29
|
+
* @param {Object} deps.diagnostics
|
|
30
|
+
* @param {Object} deps.ui - { reloadPage(hash), reloadToRoom(),
|
|
31
|
+
* showSessionOverlay(text|null), disableControls(), mute(),
|
|
32
|
+
* informTech(text), chat(message) }.
|
|
33
|
+
* @param {Function} [deps.getStorage] - () → sessionStorage (может бросить).
|
|
34
|
+
* @param {Object} [deps.factories] - конструкторы (тесты).
|
|
35
|
+
*/
|
|
36
|
+
constructor({
|
|
37
|
+
signaling,
|
|
38
|
+
ctx,
|
|
39
|
+
hostRole,
|
|
40
|
+
standby,
|
|
41
|
+
membership,
|
|
42
|
+
getHandoff,
|
|
43
|
+
getGuest,
|
|
44
|
+
getSupervisor,
|
|
45
|
+
getActiveGameId,
|
|
46
|
+
getToken,
|
|
47
|
+
prepareRoom,
|
|
48
|
+
config,
|
|
49
|
+
diagnostics,
|
|
50
|
+
ui,
|
|
51
|
+
getStorage = () => window.sessionStorage,
|
|
52
|
+
factories = {},
|
|
53
|
+
}) {
|
|
54
|
+
this._signaling = signaling;
|
|
55
|
+
this._ctx = ctx;
|
|
56
|
+
this._hostRole = hostRole;
|
|
57
|
+
this._standby = standby;
|
|
58
|
+
this._membership = membership;
|
|
59
|
+
this._getHandoff = getHandoff;
|
|
60
|
+
this._getGuest = getGuest;
|
|
61
|
+
this._getSupervisor = getSupervisor;
|
|
62
|
+
this._getActiveGameId = getActiveGameId;
|
|
63
|
+
this._getToken = getToken;
|
|
64
|
+
this._prepareRoom = prepareRoom;
|
|
65
|
+
this._config = config;
|
|
66
|
+
this._diagnostics = diagnostics;
|
|
67
|
+
this._ui = ui;
|
|
68
|
+
this._getStorage = getStorage;
|
|
69
|
+
this._factories = {
|
|
70
|
+
promotion: options => new Promotion(options),
|
|
71
|
+
loopback: (controller, socketId, options) =>
|
|
72
|
+
new LoopbackTransport(controller, socketId, options),
|
|
73
|
+
...factories,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// промоушен из контрольной точки, Worker которого ещё поднимается
|
|
77
|
+
this._inFlight = null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get inFlight() {
|
|
81
|
+
return this._inFlight;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// подписки сигналинга промоушена (лобби-режим, один раз на страницу)
|
|
85
|
+
bind() {
|
|
86
|
+
const publisher = this._signaling.publisher;
|
|
87
|
+
|
|
88
|
+
// мастер повысил эту вкладку до хоста комнаты (host-migration этап 7.4)
|
|
89
|
+
publisher.on('promote', msg => this.handlePromote(msg));
|
|
90
|
+
|
|
91
|
+
// промоушен отменён (опоздали к дедлайну): матч гасится, вкладка снова
|
|
92
|
+
// гость и ждёт нового хоста
|
|
93
|
+
publisher.on('promote_cancelled', msg => {
|
|
94
|
+
if (msg.roomId !== this._ctx.roomId) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.cancel();
|
|
99
|
+
// сорвавшаяся плановая передача: её финальная точка следующую не
|
|
100
|
+
// завершит
|
|
101
|
+
this._standby.discardFinal();
|
|
102
|
+
|
|
103
|
+
if (this._hostRole.promotion) {
|
|
104
|
+
this.abandon();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// хоста сменили, пока эта вкладка была без связи (host-migration этап
|
|
109
|
+
// 7.5)
|
|
110
|
+
publisher.on('host_revoked', msg => {
|
|
111
|
+
if (
|
|
112
|
+
this._hostRole.controller &&
|
|
113
|
+
msg.roomId === this._hostRole.room?.roomId
|
|
114
|
+
) {
|
|
115
|
+
this.demote(msg.epoch);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
handlePromote(msg) {
|
|
121
|
+
// повтор promote нашей плановой передачи режимом checkpoint: хост пропал
|
|
122
|
+
// посреди неё — финальную точку больше не ждём
|
|
123
|
+
if (
|
|
124
|
+
this._inFlight &&
|
|
125
|
+
msg.mode === 'checkpoint' &&
|
|
126
|
+
this._inFlight.degrade(msg)
|
|
127
|
+
) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (
|
|
132
|
+
!this._ctx.roomId ||
|
|
133
|
+
msg.roomId !== this._ctx.roomId ||
|
|
134
|
+
this._hostRole.controller ||
|
|
135
|
+
this._inFlight
|
|
136
|
+
) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// вход истечёт посреди хостинга — отказ сразу, мастер возьмёт следующего
|
|
141
|
+
if (!this._membership.tokenAllowsHostRole()) {
|
|
142
|
+
this._signaling.promoteFailed(msg);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const gameId = this._getActiveGameId();
|
|
147
|
+
|
|
148
|
+
// cold: точки нет — свежий матч той же комнаты после перезагрузки
|
|
149
|
+
// (бутстрап видит отложенный промоушен и занимает комнату вместо
|
|
150
|
+
// создания новой)
|
|
151
|
+
if (msg.mode === 'cold') {
|
|
152
|
+
let storage = null;
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
storage = this._getStorage();
|
|
156
|
+
} catch {
|
|
157
|
+
storage = null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!gameId || !savePendingPromotion(storage, msg, gameId)) {
|
|
161
|
+
this._signaling.promoteFailed(msg);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this._ui.reloadPage(formatRoomLink(gameId, msg.roomId));
|
|
166
|
+
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const migration = this._config.migration;
|
|
171
|
+
const promotion = this._factories.promotion({
|
|
172
|
+
promote: msg,
|
|
173
|
+
receiver: this._standby.receiver,
|
|
174
|
+
finalWaitMs: migration.finalWaitMs,
|
|
175
|
+
maxRestoreAgeMs: migration.maxRestoreAgeMs,
|
|
176
|
+
prewarm: this._standby.prewarm,
|
|
177
|
+
prepareRoom: this._prepareRoom,
|
|
178
|
+
hostSocketId: this._config.create.hostSocketId,
|
|
179
|
+
diagnostics: this._diagnostics,
|
|
180
|
+
hostCallbacks: {
|
|
181
|
+
onLobbyInfoChange: info => this._hostRole.handleLobbyInfo(info),
|
|
182
|
+
},
|
|
183
|
+
hasSession: () => this._getSupervisor()?.hasSession === true,
|
|
184
|
+
// host_migrating, если и придёт следом (старый мастер), migrate() уже
|
|
185
|
+
// не исполнит — транспорт с каналом standby останется цел
|
|
186
|
+
holdSession: () =>
|
|
187
|
+
this._getSupervisor()?.migrate({ keepTransport: true }),
|
|
188
|
+
onReady: ({ controller, room, prepared, lobbyInfo }) => {
|
|
189
|
+
this._inFlight = null;
|
|
190
|
+
this._hostRole.adopt(controller, room, prepared, {
|
|
191
|
+
promotion: {
|
|
192
|
+
mode: 'checkpoint',
|
|
193
|
+
promotion: promotion.promotion,
|
|
194
|
+
reason: msg.reason ?? null,
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
this._hostRole.startRegistration(lobbyInfo);
|
|
198
|
+
},
|
|
199
|
+
onFailed: error => {
|
|
200
|
+
this._inFlight = null;
|
|
201
|
+
console.warn('[promotion] failed:', error);
|
|
202
|
+
this._signaling.promoteFailed(promotion.promotion);
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
this._inFlight = promotion;
|
|
207
|
+
promotion.start();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// промоушен из точки ещё поднимает Worker — бросить его
|
|
211
|
+
cancel() {
|
|
212
|
+
this._inFlight?.cancel();
|
|
213
|
+
this._inFlight = null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// занять комнату не вышло (мастер отверг регистрацию или отменил
|
|
217
|
+
// промоушен): роль снимается. report — сообщить мастеру, чтобы он взял
|
|
218
|
+
// следующего кандидата, не дожидаясь дедлайна
|
|
219
|
+
abandon({ code = null, report = false } = {}) {
|
|
220
|
+
const { mode, promotion } = this._hostRole.promotion;
|
|
221
|
+
|
|
222
|
+
if (code) {
|
|
223
|
+
console.warn(`[promotion] register rejected: ${code}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (report) {
|
|
227
|
+
this._signaling.promoteFailed(promotion);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
this._hostRole.teardown();
|
|
231
|
+
|
|
232
|
+
// cold: страница — свежий хост без матча; гостем — чистым входом в ту же
|
|
233
|
+
// комнату (отложенный промоушен уже снят). checkpoint: супервизор так и
|
|
234
|
+
// ждёт нового хоста (migrating)
|
|
235
|
+
if (mode === 'cold') {
|
|
236
|
+
this._ui.reloadToRoom();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// комната занята (host_registered): матч стартует, когда вернутся люди
|
|
241
|
+
// точки (или по resumeWaitMs), свой игрок возвращается в него через
|
|
242
|
+
// loopback
|
|
243
|
+
finish({ mode, reason = null }) {
|
|
244
|
+
this._standby.teardown();
|
|
245
|
+
|
|
246
|
+
if (mode !== 'checkpoint') {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const controller = this._hostRole.controller;
|
|
251
|
+
|
|
252
|
+
controller.startAfterRestore({ waitForResume: true, reason });
|
|
253
|
+
|
|
254
|
+
const transport = this._factories.loopback(
|
|
255
|
+
controller,
|
|
256
|
+
this._config.create.hostSocketId,
|
|
257
|
+
{ resume: true },
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
transport.connect();
|
|
261
|
+
|
|
262
|
+
// свой Worker: другой попытки, кроме loopback, нет — как у хоста
|
|
263
|
+
// комнаты с создания. Комната уже занята этой вкладкой и гости
|
|
264
|
+
// переключены на неё: сбой своего игрока её не гасит
|
|
265
|
+
this._getSupervisor()?.resumeWith(transport, {
|
|
266
|
+
reconnect: null,
|
|
267
|
+
getToken: () => this._getToken(),
|
|
268
|
+
onFailed: reason => this._onOwnPlayerLost(reason),
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// хоста сменили (host_revoked или staleEpoch на reclaim) или он отдал роль
|
|
273
|
+
// сам (host_released, notice: false): матч этой вкладки гасится, свой
|
|
274
|
+
// игрок возвращается гостем к новому хосту под своим gameId (его место
|
|
275
|
+
// есть в точке беты). minEpoch — эпоха нового хоста или нижняя граница
|
|
276
|
+
demote(minEpoch, { notice = true } = {}) {
|
|
277
|
+
const handoff = this._getHandoff();
|
|
278
|
+
|
|
279
|
+
// хост уходил из комнаты, а роль сменилась без него — уходит всё равно
|
|
280
|
+
if (handoff.leaving) {
|
|
281
|
+
handoff.leave();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
this._hostRole.teardown();
|
|
286
|
+
this._standby.teardown();
|
|
287
|
+
this._ctx.epoch = minEpoch;
|
|
288
|
+
|
|
289
|
+
if (notice) {
|
|
290
|
+
this._ui.chat(buildSystemMessage('HOST_REVOKED'));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!this._ctx.roomId) {
|
|
294
|
+
this._ui.reloadPage('');
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
this._membership.sendJoinRoom();
|
|
299
|
+
|
|
300
|
+
const guest = this._getGuest();
|
|
301
|
+
const reconnect = guest.reconnect();
|
|
302
|
+
|
|
303
|
+
if (
|
|
304
|
+
!this._getSupervisor()?.resumeWith(
|
|
305
|
+
guest.openTransport(this._ctx.roomId, { resume: true }),
|
|
306
|
+
{ reconnect },
|
|
307
|
+
)
|
|
308
|
+
) {
|
|
309
|
+
this._ui.reloadToRoom();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// свой игрок преемника не вернулся в поднятый им матч (секрета нет, отказ
|
|
314
|
+
// RESUME, loopback закрыт): роль хоста остаётся — Worker, пиры, heartbeat
|
|
315
|
+
// и поток точек живут, гости играют. Перезагрузка убила бы матч всех
|
|
316
|
+
_onOwnPlayerLost(reason) {
|
|
317
|
+
console.warn(`[promotion] own player not restored: ${reason}`);
|
|
318
|
+
this._ui.showSessionOverlay(null);
|
|
319
|
+
this._ui.disableControls();
|
|
320
|
+
this._ui.mute();
|
|
321
|
+
this._ui.informTech(
|
|
322
|
+
'Your player could not be restored — the room keeps running for the others.',
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|