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
package/src/client/main.js
CHANGED
|
@@ -59,14 +59,17 @@ import { sanitizeMessage } from '../lib/sanitizers.js';
|
|
|
59
59
|
import { validateAuth } from '../lib/validators.js';
|
|
60
60
|
import SoundManager from './SoundManager.js';
|
|
61
61
|
import SignalingClient from './network/SignalingClient.js';
|
|
62
|
-
import WebRtcManager from './network/WebRtcManager.js';
|
|
63
|
-
import HostController from './network/HostController.js';
|
|
64
|
-
import HostConnectionManager from './network/HostConnectionManager.js';
|
|
65
62
|
import LoopbackTransport from './network/LoopbackTransport.js';
|
|
66
63
|
import WebSocketTransport from './network/WebSocketTransport.js';
|
|
67
64
|
import InlineHostBridge from './network/InlineHostBridge.js';
|
|
68
|
-
import {
|
|
65
|
+
import SessionSupervisor, {
|
|
66
|
+
SESSION_STATES,
|
|
67
|
+
} from './network/SessionSupervisor.js';
|
|
68
|
+
import FpsMeter from './lib/FpsMeter.js';
|
|
69
|
+
import HostUnloadGuard from './lib/hostUnloadGuard.js';
|
|
70
|
+
import { createPageReload } from './lib/pageReload.js';
|
|
69
71
|
import {
|
|
72
|
+
isKickClose,
|
|
70
73
|
POLICY_CLOSE_INFORMS,
|
|
71
74
|
shouldReloadAfterClose,
|
|
72
75
|
} from './network/policyClose.js';
|
|
@@ -79,13 +82,38 @@ import LobbyAuthCtrl from './components/controller/LobbyAuth.js';
|
|
|
79
82
|
import GamesModel from './components/model/Games.js';
|
|
80
83
|
import GamesView from './components/view/Games.js';
|
|
81
84
|
import GamesCtrl from './components/controller/Games.js';
|
|
85
|
+
import { createRoomContext } from './session/roomContext.js';
|
|
86
|
+
import { createHostRoomPrep } from './session/hostRoomPrep.js';
|
|
87
|
+
import Membership from './session/Membership.js';
|
|
88
|
+
import StandbyRole from './session/StandbyRole.js';
|
|
89
|
+
import HostRole from './session/HostRole.js';
|
|
90
|
+
import HandoffFlow from './session/HandoffFlow.js';
|
|
91
|
+
import PromotionFlow from './session/PromotionFlow.js';
|
|
92
|
+
import GuestSession from './session/GuestSession.js';
|
|
93
|
+
import RouteBoot from './session/RouteBoot.js';
|
|
82
94
|
import ClientReportsModel from './components/model/ClientReports.js';
|
|
83
95
|
import ClientReportsView from './components/view/ClientReports.js';
|
|
84
96
|
import ClientReportsCtrl from './components/controller/ClientReports.js';
|
|
97
|
+
import RoomMenuModel from './components/model/RoomMenu.js';
|
|
98
|
+
import RoomMenuView from './components/view/RoomMenu.js';
|
|
99
|
+
import RoomMenuCtrl from './components/controller/RoomMenu.js';
|
|
100
|
+
import {
|
|
101
|
+
absoluteLink,
|
|
102
|
+
decideExitRoute,
|
|
103
|
+
formatGameLink,
|
|
104
|
+
formatRoomLink,
|
|
105
|
+
setRoute,
|
|
106
|
+
} from './lib/roomLink.js';
|
|
107
|
+
import {
|
|
108
|
+
CHANGE_HOST_VOTE,
|
|
109
|
+
answerValue,
|
|
110
|
+
parseChangeHost,
|
|
111
|
+
} from './lib/hostVoteCommand.js';
|
|
85
112
|
import BakingProvider from './providers/BakingProvider.js';
|
|
86
113
|
import DependencyProvider from './providers/DependencyProvider.js';
|
|
87
114
|
import wsports from '../config/wsports.js';
|
|
88
115
|
import GAME_CODES from '../config/gameCodes.js';
|
|
116
|
+
import { buildSystemMessage } from '../host/meta/modules/chat/systemMessages.js';
|
|
89
117
|
import {
|
|
90
118
|
fetchGamesManifest,
|
|
91
119
|
fetchGameManifest as fetchGamePluginManifest,
|
|
@@ -154,7 +182,9 @@ const isLobbyMode = bootMode === 'lobby';
|
|
|
154
182
|
// свой бокс; SDK (solo) — только если встраивающий передал reportUrl
|
|
155
183
|
const diagnostics = createDiagnostics({
|
|
156
184
|
url:
|
|
157
|
-
bootMode === 'solo'
|
|
185
|
+
bootMode === 'solo'
|
|
186
|
+
? (boot.reportUrl ?? null)
|
|
187
|
+
: lobbyConfig.clientReportUrl,
|
|
158
188
|
context: {
|
|
159
189
|
mode: bootMode,
|
|
160
190
|
role: 'client',
|
|
@@ -253,6 +283,8 @@ const PS_CHAT_DATA = wsports.server.CHAT_DATA;
|
|
|
253
283
|
const PS_VOTE_DATA = wsports.server.VOTE_DATA;
|
|
254
284
|
const PS_KEYSET_DATA = wsports.server.KEYSET_DATA;
|
|
255
285
|
const PS_ACCOLADES_DATA = wsports.server.ACCOLADES_DATA;
|
|
286
|
+
const PS_SESSION_DATA = wsports.server.SESSION_DATA;
|
|
287
|
+
const PS_RESUME_RESULT = wsports.server.RESUME_RESULT;
|
|
256
288
|
|
|
257
289
|
// PC (client ports): порты получения данных от клиента
|
|
258
290
|
const PC_CONFIG_READY = wsports.client.CONFIG_READY;
|
|
@@ -264,6 +296,7 @@ const PC_KEYS_DATA = wsports.client.KEYS_DATA;
|
|
|
264
296
|
const PC_CHAT_DATA = wsports.client.CHAT_DATA;
|
|
265
297
|
const PC_VOTE_DATA = wsports.client.VOTE_DATA;
|
|
266
298
|
const PC_PONG = wsports.client.PONG;
|
|
299
|
+
const PC_LEAVE = wsports.client.LEAVE;
|
|
267
300
|
|
|
268
301
|
// сигнальный WebSocket мастера (лобби + установка P2P); игровой трафик идёт
|
|
269
302
|
// по WebRTC (transport), не через мастер
|
|
@@ -273,12 +306,14 @@ const signaling = isLobbyMode
|
|
|
273
306
|
? new SignalingClient(`${wsProtocol}//${location.host}/`)
|
|
274
307
|
: null;
|
|
275
308
|
|
|
276
|
-
//
|
|
277
|
-
|
|
309
|
+
// супервизор сессии (host-migration этап 4): держит текущий транспорт к
|
|
310
|
+
// хосту и переживает его обрыв — переподключается и возвращает место в
|
|
311
|
+
// матче. Создаётся при входе в комнату (или в solo/dedicated-матч)
|
|
312
|
+
let supervisor = null;
|
|
278
313
|
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
314
|
+
// оверлей «Reconnecting…» на время переподключения (только лобби-режим)
|
|
315
|
+
const sessionOverlay = document.getElementById('session-overlay');
|
|
316
|
+
const sessionOverlayText = document.getElementById('session-overlay-text');
|
|
282
317
|
|
|
283
318
|
const modules = {};
|
|
284
319
|
|
|
@@ -332,6 +367,9 @@ const GAME_ROUND_START_CODE = GAME_CODES.roundStart[0];
|
|
|
332
367
|
// показан ли терминальный tech-код (кик, полная комната): причина закрытия
|
|
333
368
|
// соединения важнее общего сообщения handleDisconnect
|
|
334
369
|
let terminalInformShown = false;
|
|
370
|
+
// ключ последнего терминального tech-кода: по нему лобби-режим отличает кик
|
|
371
|
+
// (policyClose.isKickClose) от закрытия комнаты — в P2P кода закрытия нет
|
|
372
|
+
let terminalTechKey = null;
|
|
335
373
|
// снятие предыдущего animationend-листенера логотипа при повторном
|
|
336
374
|
// playLogoRoundStart — иначе листенеры копятся при частых стартах раунда
|
|
337
375
|
let logoAnimationEndHandler = null;
|
|
@@ -342,6 +380,10 @@ let entitiesOnCanvas = {}; // сущности, отображаемые на п
|
|
|
342
380
|
let currentMapSetId; // текущий id набора конструкторов для карт
|
|
343
381
|
const socketMethods = []; // методы для обработки сокет-данных
|
|
344
382
|
|
|
383
|
+
// модули матча запущены (AUTH_RESULT без ошибки): повторный успешный
|
|
384
|
+
// AUTH_RESULT в живой сессии — ошибка протокола
|
|
385
|
+
let sessionStarted = false;
|
|
386
|
+
|
|
345
387
|
// клиентское ядро (WASM, срез 2.6): интерполяция снапшотов, предикт своего
|
|
346
388
|
// танка, визуальный спавн выстрелов и распаковка кадров v3 — создаётся при
|
|
347
389
|
// получении конфига; wasm — результат init() для zero-copy чтения памяти
|
|
@@ -371,6 +413,13 @@ let snapshotKeysById = null;
|
|
|
371
413
|
|
|
372
414
|
// config data
|
|
373
415
|
socketMethods[PS_CONFIG_DATA] = async data => {
|
|
416
|
+
// повторное рукопожатие в живой сессии (например, хост не понял resume)
|
|
417
|
+
// построило бы второе ядро и второй Application поверх первых
|
|
418
|
+
if (clientCore) {
|
|
419
|
+
handleProtocolError('CONFIG_DATA');
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
374
423
|
gameSets = data.parts.gameSets;
|
|
375
424
|
entitiesOnCanvas = data.parts.entitiesOnCanvas;
|
|
376
425
|
|
|
@@ -587,6 +636,12 @@ socketMethods[PS_AUTH_RESULT] = async err => {
|
|
|
587
636
|
}
|
|
588
637
|
|
|
589
638
|
if (!err) {
|
|
639
|
+
if (sessionStarted) {
|
|
640
|
+
handleProtocolError('AUTH_RESULT');
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
sessionStarted = true;
|
|
590
645
|
await soundManager.init(soundData);
|
|
591
646
|
runModules(modulesConfig);
|
|
592
647
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
@@ -599,13 +654,33 @@ socketMethods[PS_AUTH_RESULT] = async err => {
|
|
|
599
654
|
}
|
|
600
655
|
}
|
|
601
656
|
|
|
657
|
+
roomMenu?.setInPanel(initIdList.includes('panel'));
|
|
658
|
+
|
|
602
659
|
sending(PC_MODULES_READY);
|
|
660
|
+
|
|
661
|
+
// вход в комнату состоялся — мастер узнаёт состав комнаты от участника
|
|
662
|
+
// (хост — участник своей комнаты с register_host)
|
|
663
|
+
if (roomCtx.roomId && !hostRole.controller) {
|
|
664
|
+
membership.sendJoinRoom();
|
|
665
|
+
}
|
|
603
666
|
}
|
|
604
667
|
};
|
|
605
668
|
|
|
606
669
|
// map data
|
|
607
670
|
socketMethods[PS_MAP_DATA] = data => {
|
|
608
671
|
lastMapData = data;
|
|
672
|
+
|
|
673
|
+
// карта пакета возобновления (host-migration): участник уже в матче —
|
|
674
|
+
// это не загрузка (setLoading закончил бы пакет), MAP_READY хост не ждёт.
|
|
675
|
+
// Признак — метка хоста, не supervisor.resuming: неготовому участнику
|
|
676
|
+
// (карта сменилась за паузу) хост в том же окне шлёт обычную загрузку
|
|
677
|
+
if (data?.resume === true) {
|
|
678
|
+
applyMapData(data, { notifyHost: false });
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// хост законно молчит до первого кадра новой карты (сторожок тишины)
|
|
683
|
+
supervisor?.setLoading(true);
|
|
609
684
|
applyMapData(data);
|
|
610
685
|
};
|
|
611
686
|
|
|
@@ -751,9 +826,17 @@ socketMethods[PS_FIRST_SHOT_DATA] = data => {
|
|
|
751
826
|
const [game, camera] = data;
|
|
752
827
|
|
|
753
828
|
applyShot(game, camera);
|
|
829
|
+
supervisor?.setLoading(false);
|
|
830
|
+
|
|
831
|
+
// кадр пакета возобновления (host-migration этап 4): участник уже в
|
|
832
|
+
// матче — подтверждать нечего, разовый автостарт уже отработал
|
|
833
|
+
if (supervisor?.resuming) {
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
754
836
|
|
|
755
837
|
// подтверждение получения первого шота
|
|
756
838
|
sending(PC_FIRST_SHOT_READY);
|
|
839
|
+
supervisor?.enterGame();
|
|
757
840
|
|
|
758
841
|
// solo: выход из наблюдателей и чат-команды игры (боты) — см. autostart.js
|
|
759
842
|
runAutostart();
|
|
@@ -877,6 +960,7 @@ socketMethods[PS_TECH_INFORM_DATA] = data => {
|
|
|
877
960
|
// терминальные коды (кик, полная комната) — причина закрытия соединения,
|
|
878
961
|
// последующий handleDisconnect не должен затирать её общим сообщением
|
|
879
962
|
terminalInformShown = key !== TECH_LOADING_CODE;
|
|
963
|
+
terminalTechKey = terminalInformShown ? key : null;
|
|
880
964
|
} else {
|
|
881
965
|
message = data;
|
|
882
966
|
}
|
|
@@ -887,6 +971,7 @@ socketMethods[PS_TECH_INFORM_DATA] = data => {
|
|
|
887
971
|
} else {
|
|
888
972
|
modules.controls?.enableKeys();
|
|
889
973
|
terminalInformShown = false;
|
|
974
|
+
terminalTechKey = null;
|
|
890
975
|
techInformer.textContent = '';
|
|
891
976
|
techInformer.style.display = 'none';
|
|
892
977
|
}
|
|
@@ -929,6 +1014,21 @@ socketMethods[PS_CLEAR] = function (setIdList) {
|
|
|
929
1014
|
soundManager.reset();
|
|
930
1015
|
};
|
|
931
1016
|
|
|
1017
|
+
// session data: секрет места в матче (host-migration этап 4) — с ним
|
|
1018
|
+
// супервизор вернётся после обрыва. Последнее сообщение пакета
|
|
1019
|
+
// возобновления: набор клавиш в пакете уже пришёл, и удерживаемые клавиши,
|
|
1020
|
+
// которые хост отпустил на время паузы, можно нажать снова
|
|
1021
|
+
socketMethods[PS_SESSION_DATA] = data => {
|
|
1022
|
+
if (supervisor?.setSession(data ?? {})) {
|
|
1023
|
+
modules.controls?.resendHeld();
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
// resume result: ответ хоста на RESUME_REQUEST
|
|
1028
|
+
socketMethods[PS_RESUME_RESULT] = data => {
|
|
1029
|
+
supervisor?.resumeResult(data);
|
|
1030
|
+
};
|
|
1031
|
+
|
|
932
1032
|
// console: логи авторитетной половины (Worker изолирован от DevTools вкладки —
|
|
933
1033
|
// иначе его события в браузере не видны вовсе, этап 6 плана plan/done/ai-debug)
|
|
934
1034
|
socketMethods[PS_CONSOLE] = data => {
|
|
@@ -1183,7 +1283,7 @@ function runModules(data) {
|
|
|
1183
1283
|
sending(PC_KEYS_DATA, `${inputSeq}:aim:${wx}:${wy}:${flags}`);
|
|
1184
1284
|
});
|
|
1185
1285
|
chatModel.publisher.on('socket', handleChatSend);
|
|
1186
|
-
voteModel.publisher.on('socket',
|
|
1286
|
+
voteModel.publisher.on('socket', handleVoteSend);
|
|
1187
1287
|
|
|
1188
1288
|
//==========================================//
|
|
1189
1289
|
// Рендер-цикл интерполяции
|
|
@@ -1212,44 +1312,47 @@ function openMode(mode) {
|
|
|
1212
1312
|
// reliable=false — по ненадёжному state-каналу (только pong: замер RTT
|
|
1213
1313
|
// должен отражать сетевой путь, а не reliable-поток с ретрансмиссиями)
|
|
1214
1314
|
function sending(name, data, reliable = true) {
|
|
1215
|
-
|
|
1315
|
+
supervisor?.send(JSON.stringify([name, data]), reliable);
|
|
1216
1316
|
}
|
|
1217
1317
|
|
|
1218
|
-
//
|
|
1219
|
-
//
|
|
1220
|
-
// голос против самого себя (server-rating этап 2, замена /ban). Причина
|
|
1221
|
-
// обязательна (публично не отображается). Остальной чат уходит хосту
|
|
1318
|
+
// отправка сообщения чата хосту (точка перехвата команд, адресованных
|
|
1319
|
+
// мастеру, а не хосту)
|
|
1222
1320
|
function handleChatSend(message) {
|
|
1223
|
-
const
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1321
|
+
const changeHost = parseChangeHost(message, bootMode);
|
|
1322
|
+
|
|
1323
|
+
if (changeHost === null) {
|
|
1324
|
+
sending(PC_CHAT_DATA, message);
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// голосование «Change host» считает мастер (этап 10): хост не должен
|
|
1329
|
+
// видеть и не может заблокировать голосование против себя
|
|
1330
|
+
const notice = key => modules.chat?.add(buildSystemMessage(key));
|
|
1331
|
+
|
|
1332
|
+
if (changeHost === 'usage') {
|
|
1333
|
+
notice('HOST_VOTE_USAGE');
|
|
1334
|
+
} else if (hostRole.controller) {
|
|
1335
|
+
notice('HOST_VOTE_IS_HOST');
|
|
1336
|
+
} else if (!signaling?.connected || !roomCtx.roomId) {
|
|
1337
|
+
notice('HOST_VOTE_OFFLINE');
|
|
1338
|
+
} else {
|
|
1339
|
+
signaling.hostVoteStart(roomCtx.roomId);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// ответ в окне голосования: «Change host?» — мастеру, остальное — хосту
|
|
1344
|
+
function handleVoteSend(data) {
|
|
1345
|
+
if (Array.isArray(data) && data[0] === CHANGE_HOST_VOTE) {
|
|
1346
|
+
const value = answerValue(data[1]);
|
|
1347
|
+
|
|
1348
|
+
if (guest.hostVote && value && signaling?.connected) {
|
|
1349
|
+
signaling.hostVoteAnswer({ ...guest.hostVote, value });
|
|
1247
1350
|
}
|
|
1248
1351
|
|
|
1249
1352
|
return;
|
|
1250
1353
|
}
|
|
1251
1354
|
|
|
1252
|
-
sending(
|
|
1355
|
+
sending(PC_VOTE_DATA, data);
|
|
1253
1356
|
}
|
|
1254
1357
|
|
|
1255
1358
|
// распаковывает данные
|
|
@@ -1273,9 +1376,12 @@ function handleVisibilityChange() {
|
|
|
1273
1376
|
if (document.visibilityState === 'hidden') {
|
|
1274
1377
|
hiddenAt = performance.now();
|
|
1275
1378
|
soundManager.mute();
|
|
1276
|
-
// иначе включение звука при возвращении
|
|
1379
|
+
// иначе включение звука при возвращении (кроме паузы переподключения и
|
|
1380
|
+
// смены хоста — его включит возобновление сессии)
|
|
1277
1381
|
} else {
|
|
1278
|
-
|
|
1382
|
+
if (!isSessionPaused()) {
|
|
1383
|
+
soundManager.unmute();
|
|
1384
|
+
}
|
|
1279
1385
|
|
|
1280
1386
|
const hiddenMs = hiddenAt === null ? 0 : performance.now() - hiddenAt;
|
|
1281
1387
|
|
|
@@ -1422,6 +1528,11 @@ function handleMessage(data) {
|
|
|
1422
1528
|
// бинарный кадр (snapshot, порт SHOT_DATA) — в ядро: распаковка, вставка
|
|
1423
1529
|
// в буфер по seq, reconciliation предикта по player-блоку
|
|
1424
1530
|
if (data instanceof ArrayBuffer) {
|
|
1531
|
+
// бета помнит seq последнего кадра хоста (seqFloor, этап 6) — до
|
|
1532
|
+
// push_frame: ядро может забрать буфер
|
|
1533
|
+
standby.noteFrame(data);
|
|
1534
|
+
// восстановленный матч пошёл — сторожок тишины снимает фору
|
|
1535
|
+
supervisor?.noteFrame();
|
|
1425
1536
|
clientCore?.push_frame(new Uint8Array(data), performance.now());
|
|
1426
1537
|
|
|
1427
1538
|
return;
|
|
@@ -1432,11 +1543,14 @@ function handleMessage(data) {
|
|
|
1432
1543
|
dispatchSocketMessage(socketMethods, unpacking(data));
|
|
1433
1544
|
}
|
|
1434
1545
|
|
|
1435
|
-
//
|
|
1436
|
-
//
|
|
1437
|
-
//
|
|
1438
|
-
//
|
|
1546
|
+
// терминальное закрытие сессии (супервизор: возвращаться некуда — кик,
|
|
1547
|
+
// политика, уход хоста, окно переподключения истекло). Останавливаем
|
|
1548
|
+
// рендер, показываем заглушку и возвращаемся в лобби перезагрузкой.
|
|
1549
|
+
// closeCode приходит только от WebSocket-транспорта (dedicated), остальные
|
|
1550
|
+
// транспорты эмитят close без него
|
|
1439
1551
|
function handleDisconnect(closeCode) {
|
|
1552
|
+
showSessionOverlay(null);
|
|
1553
|
+
|
|
1440
1554
|
// app.stop() здесь не зовём: при sharedTicker это Ticker.shared.stop()
|
|
1441
1555
|
// глобально, а autoStart вернёт тикер к жизни при первом add() из любого
|
|
1442
1556
|
// part'а — уже без renderTick. Рендер снят строкой выше, страницу и так
|
|
@@ -1461,18 +1575,21 @@ function handleDisconnect(closeCode) {
|
|
|
1461
1575
|
soundManager.destroy();
|
|
1462
1576
|
modules.controls?.disableKeys();
|
|
1463
1577
|
|
|
1464
|
-
//
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1578
|
+
// гость выбыл из матча (кик уводит на главную без перезагрузки — сессия
|
|
1579
|
+
// сигналинга жива): снять членство, иначе вкладка «призраком» числится в
|
|
1580
|
+
// комнате. Best-effort; хост комнату так не покидает — его снимает мастер
|
|
1581
|
+
if (isLobbyMode && roomCtx.roomId && !hostRole.controller) {
|
|
1582
|
+
signaling.leaveRoom(roomCtx.roomId);
|
|
1468
1583
|
}
|
|
1469
1584
|
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1585
|
+
// если мы были хостом — гасим комнату: heartbeat, WebRTC-пиры, Worker
|
|
1586
|
+
promotionFlow.cancel();
|
|
1587
|
+
hostRole.teardown();
|
|
1588
|
+
standby.teardown();
|
|
1589
|
+
roomCtx.roomId = null;
|
|
1590
|
+
membership.forget();
|
|
1591
|
+
membership.clearTokenCapsTimer();
|
|
1592
|
+
refreshRoomControls();
|
|
1476
1593
|
|
|
1477
1594
|
// solo: хост крутится в этом же потоке — его таймеры переживут матч
|
|
1478
1595
|
inlineHost?.destroy();
|
|
@@ -1488,7 +1605,7 @@ function handleDisconnect(closeCode) {
|
|
|
1488
1605
|
socketMethods[PS_TECH_INFORM_DATA](
|
|
1489
1606
|
policyInform ??
|
|
1490
1607
|
(isLobbyMode
|
|
1491
|
-
? 'Host left — the room is closed.
|
|
1608
|
+
? 'Host left — the room is closed. Finding another room…'
|
|
1492
1609
|
: 'The match is over — connection to the host is closed.'),
|
|
1493
1610
|
);
|
|
1494
1611
|
}
|
|
@@ -1496,9 +1613,36 @@ function handleDisconnect(closeCode) {
|
|
|
1496
1613
|
// в solo перезагружаться некуда: лобби нет, а матч поднимается с нуля.
|
|
1497
1614
|
// В dedicated сервер жив — переподключение перезагрузкой уместно, но не
|
|
1498
1615
|
// тогда, когда он сам нас и отбил по политике (network/policyClose.js)
|
|
1499
|
-
if (bootMode
|
|
1500
|
-
|
|
1616
|
+
if (bootMode === 'solo' || !shouldReloadAfterClose(closeCode)) {
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
if (!isLobbyMode) {
|
|
1621
|
+
setTimeout(() => reloadPage(), 3000);
|
|
1622
|
+
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
roomMenu?.setLink(null);
|
|
1627
|
+
|
|
1628
|
+
// лобби-режим (host-migration, этап 3): кик и несостоявшийся старт комнаты
|
|
1629
|
+
// — на главную без перезагрузки (причина остаётся в #tech-informer, клик
|
|
1630
|
+
// по нему открывает лобби); закрытие комнаты — в быструю игру той же игры
|
|
1631
|
+
const exit = decideExitRoute({
|
|
1632
|
+
kicked: isKickClose(closeCode, terminalTechKey) || roomCtx.startFailed,
|
|
1633
|
+
gameId: activeGameManifest?.id,
|
|
1634
|
+
});
|
|
1635
|
+
|
|
1636
|
+
if (exit.reload === undefined) {
|
|
1637
|
+
setRoute(exit.route);
|
|
1638
|
+
techInformer.addEventListener('click', () => reloadPage(), {
|
|
1639
|
+
once: true,
|
|
1640
|
+
});
|
|
1641
|
+
|
|
1642
|
+
return;
|
|
1501
1643
|
}
|
|
1644
|
+
|
|
1645
|
+
setTimeout(() => reloadPage(exit.reload), 3000);
|
|
1502
1646
|
}
|
|
1503
1647
|
|
|
1504
1648
|
/**
|
|
@@ -1508,269 +1652,405 @@ function handleDisconnect(closeCode) {
|
|
|
1508
1652
|
* teardown у SDK нет специально — путь останова один на все режимы.
|
|
1509
1653
|
*/
|
|
1510
1654
|
export function stopGame() {
|
|
1511
|
-
|
|
1655
|
+
supervisor?.close();
|
|
1512
1656
|
}
|
|
1513
1657
|
|
|
1514
|
-
//
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1658
|
+
// ***** супервизор сессии (host-migration этап 4) ***** //
|
|
1659
|
+
|
|
1660
|
+
// берёт транспорт под надзор. reconnect — фабрика попыток переподключения
|
|
1661
|
+
// (только гость в лобби); без неё любое закрытие терминально, как раньше
|
|
1662
|
+
function startSession(transport, reconnect = null) {
|
|
1663
|
+
supervisor = new SessionSupervisor({
|
|
1664
|
+
onMessage: handleMessage,
|
|
1665
|
+
onTerminal: handleDisconnect,
|
|
1666
|
+
onStateChange: handleSessionState,
|
|
1667
|
+
onResumed: handleSessionResumed,
|
|
1668
|
+
onResumeRejected: handleResumeRejected,
|
|
1669
|
+
onHostLost: reportHostUnreachable,
|
|
1670
|
+
onColdRestart: handleColdRestart,
|
|
1671
|
+
// кик (TECH_INFORM перед закрытием) и отказы политики не лечатся
|
|
1672
|
+
// переподключением
|
|
1673
|
+
isTerminalClose: closeCode =>
|
|
1674
|
+
terminalInformShown || !shouldReloadAfterClose(closeCode),
|
|
1675
|
+
reconnect,
|
|
1676
|
+
timing: lobbyConfig.session,
|
|
1531
1677
|
});
|
|
1532
1678
|
|
|
1533
|
-
|
|
1534
|
-
'window.__vimpDebug is available: dump, startRecording, stopRecording, divergence',
|
|
1535
|
-
);
|
|
1679
|
+
supervisor.attach(transport);
|
|
1536
1680
|
}
|
|
1537
1681
|
|
|
1538
|
-
//
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
if (typeof RTCPeerConnection !== 'undefined') {
|
|
1543
|
-
return true;
|
|
1682
|
+
// оверлей сессии: text — показать с текстом, null — скрыть
|
|
1683
|
+
function showSessionOverlay(text) {
|
|
1684
|
+
if (!sessionOverlay) {
|
|
1685
|
+
return;
|
|
1544
1686
|
}
|
|
1545
1687
|
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1688
|
+
if (text) {
|
|
1689
|
+
sessionOverlayText.textContent = text;
|
|
1690
|
+
sessionOverlay.style.display = 'flex';
|
|
1691
|
+
} else {
|
|
1692
|
+
sessionOverlayText.textContent = '';
|
|
1693
|
+
sessionOverlay.style.display = 'none';
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1550
1696
|
|
|
1551
|
-
|
|
1697
|
+
// сессия на паузе: транспорта к хосту нет, мир стоит
|
|
1698
|
+
function isSessionPaused() {
|
|
1699
|
+
return (
|
|
1700
|
+
supervisor?.state === SESSION_STATES.reconnecting ||
|
|
1701
|
+
supervisor?.state === SESSION_STATES.migrating
|
|
1702
|
+
);
|
|
1552
1703
|
}
|
|
1553
1704
|
|
|
1554
|
-
//
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1705
|
+
// пауза переподключения или смены хоста: мир стоит, ввод и звук выключены
|
|
1706
|
+
// (зацикленные звуки не должны гудеть одной нотой всю паузу). Рендер не
|
|
1707
|
+
// останавливается — кадр стоит, независимые от снапшотов анимации идут
|
|
1708
|
+
function handleSessionState(state, prev) {
|
|
1709
|
+
if (state === SESSION_STATES.migrating) {
|
|
1710
|
+
showSessionOverlay('Switching host…');
|
|
1711
|
+
modules.controls?.disableKeys();
|
|
1712
|
+
soundManager.mute();
|
|
1713
|
+
} else if (state === SESSION_STATES.reconnecting) {
|
|
1714
|
+
// после смены хоста оверлей тот же до конца возобновления
|
|
1715
|
+
if (prev !== SESSION_STATES.migrating) {
|
|
1716
|
+
showSessionOverlay('Reconnecting…');
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
modules.controls?.disableKeys();
|
|
1720
|
+
soundManager.mute();
|
|
1721
|
+
} else if (state === SESSION_STATES.inGame) {
|
|
1722
|
+
showSessionOverlay(null);
|
|
1558
1723
|
}
|
|
1724
|
+
}
|
|
1559
1725
|
|
|
1560
|
-
|
|
1726
|
+
// транспорт к хосту оборвался в матче: мастер узнаёт об этом от гостя
|
|
1727
|
+
// раньше, чем сам (host-migration этап 7.2) — проба хоста или его смена.
|
|
1728
|
+
// Эпоха — хоста, к которому был транспорт: чужую мастер отбросит
|
|
1729
|
+
function reportHostUnreachable() {
|
|
1730
|
+
if (isLobbyMode) {
|
|
1731
|
+
guest.reportHostUnreachable();
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1561
1734
|
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1735
|
+
// новый хост поднял матч заново (cold): возобновлять нечего — чистый вход в
|
|
1736
|
+
// ту же комнату с полным рукопожатием
|
|
1737
|
+
function handleColdRestart() {
|
|
1738
|
+
reloadToRoom();
|
|
1739
|
+
}
|
|
1565
1740
|
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1741
|
+
// место возвращено: предсказание начинается с чистого листа (вводы,
|
|
1742
|
+
// ушедшие в оборванный транспорт, хост не получит никогда — переигрывать
|
|
1743
|
+
// их нельзя), голосование хоста паузу не пережило. Дальше хост шлёт пакет
|
|
1744
|
+
// входа: CLEAR, полный кадр, набор клавиш, SESSION_DATA
|
|
1745
|
+
function handleSessionResumed() {
|
|
1746
|
+
clientCore?.reset();
|
|
1747
|
+
// «Change host?» ведёт мастер — оно паузу переживает
|
|
1748
|
+
modules.vote?.removeHostVotes();
|
|
1749
|
+
modules.controls?.enableKeys();
|
|
1569
1750
|
|
|
1570
|
-
|
|
1751
|
+
if (document.visibilityState !== 'hidden') {
|
|
1752
|
+
soundManager.unmute();
|
|
1753
|
+
}
|
|
1571
1754
|
}
|
|
1572
1755
|
|
|
1573
|
-
//
|
|
1574
|
-
//
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
socketMethods[PS_TECH_INFORM_DATA](
|
|
1581
|
-
'This browser cannot be a host: ES module Web Workers are ' +
|
|
1582
|
-
'unsupported. You can still join existing rooms.',
|
|
1583
|
-
);
|
|
1756
|
+
// место потеряно (ожидание истекло, ключ не принят): чистый вход в ту же
|
|
1757
|
+
// комнату с полным рукопожатием
|
|
1758
|
+
function handleResumeRejected(reason) {
|
|
1759
|
+
console.warn(`[session] resume rejected: ${reason}`);
|
|
1760
|
+
showSessionOverlay(null);
|
|
1761
|
+
reloadToRoom();
|
|
1762
|
+
}
|
|
1584
1763
|
|
|
1585
|
-
|
|
1586
|
-
|
|
1764
|
+
// ссылка на текущую комнату (иначе — на быструю игру той же игры)
|
|
1765
|
+
function reloadToRoom() {
|
|
1766
|
+
const gameId = activeGameManifest?.id;
|
|
1587
1767
|
|
|
1588
|
-
if (!
|
|
1768
|
+
if (!gameId) {
|
|
1769
|
+
reloadPage('');
|
|
1589
1770
|
return;
|
|
1590
1771
|
}
|
|
1591
1772
|
|
|
1592
|
-
|
|
1593
|
-
|
|
1773
|
+
reloadPage(
|
|
1774
|
+
roomCtx.roomId
|
|
1775
|
+
? formatRoomLink(gameId, roomCtx.roomId)
|
|
1776
|
+
: formatGameLink(gameId),
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1594
1779
|
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1780
|
+
// хост начал рукопожатие заново посреди живой сессии (порт — какой
|
|
1781
|
+
// именно). Второе ядро/Application не строим: в лобби — чистый вход в ту же
|
|
1782
|
+
// комнату, в остальных режимах только журнал
|
|
1783
|
+
function handleProtocolError(port) {
|
|
1784
|
+
diagnostics.warn('engine.session.protocol', { port }, { source: 'client' });
|
|
1598
1785
|
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
version: activeGameManifest.version,
|
|
1604
|
-
hostEntryUrl: activeGameManifest.entries.host,
|
|
1605
|
-
wasmUrl: activeGameManifest.entries.wasm,
|
|
1606
|
-
};
|
|
1786
|
+
if (isLobbyMode) {
|
|
1787
|
+
reloadToRoom();
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1607
1790
|
|
|
1608
|
-
|
|
1609
|
-
// недоступность каталога некритична — Worker возьмёт карты из бандла
|
|
1610
|
-
try {
|
|
1611
|
-
const catalog = await fetchMasterMaps();
|
|
1791
|
+
// БУТСТРАП: лобби и установка P2P через мастер-сервер
|
|
1612
1792
|
|
|
1613
|
-
|
|
1614
|
-
hostMapsVersion = catalog.version;
|
|
1615
|
-
} catch (e) {
|
|
1616
|
-
console.warn('[maps] master catalog unavailable, using bundled maps:', e);
|
|
1617
|
-
}
|
|
1793
|
+
let lobby = null;
|
|
1618
1794
|
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1795
|
+
// id вкладки — ключ участника комнаты на мастере; живёт в памяти (не в
|
|
1796
|
+
// storage): две вкладки одного профиля — два участника
|
|
1797
|
+
const memberId = isLobbyMode ? crypto.randomUUID() : null;
|
|
1798
|
+
|
|
1799
|
+
// меню комнаты внутри матча (этапы 3 и 8d) и защита вкладки хоста от
|
|
1800
|
+
// случайного закрытия (этап 8.4): только лобби-режим
|
|
1801
|
+
let roomMenu = null;
|
|
1802
|
+
let unloadGuard = null;
|
|
1803
|
+
// любая программная перезагрузка — только через неё (client/lib/pageReload.js)
|
|
1804
|
+
const reloadPage = createPageReload({ getGuard: () => unloadGuard });
|
|
1805
|
+
|
|
1806
|
+
// ***** сценарии комнаты (client/session/*): здесь только сборка ***** //
|
|
1807
|
+
|
|
1808
|
+
const isPageHidden = () => document.visibilityState === 'hidden';
|
|
1809
|
+
const informTech = text => socketMethods[PS_TECH_INFORM_DATA](text);
|
|
1810
|
+
const chatNotice = message => modules.chat?.add(message);
|
|
1811
|
+
const getSupervisor = () => supervisor;
|
|
1812
|
+
const getToken = () => lobbyAuthModel.getToken();
|
|
1813
|
+
const getTokenExpiresAt = () => lobbyAuthModel.getTokenExpiresAt();
|
|
1814
|
+
|
|
1815
|
+
// комната вкладки: id, эпоха хоста, членство, вход и несостоявшийся старт
|
|
1816
|
+
const roomCtx = createRoomContext();
|
|
1817
|
+
|
|
1818
|
+
const hostRoomPrep = createHostRoomPrep({
|
|
1819
|
+
getActiveGame: () => activeGameManifest,
|
|
1820
|
+
config: lobbyConfig,
|
|
1821
|
+
isDevBuild,
|
|
1822
|
+
fetchGamePluginManifest,
|
|
1823
|
+
});
|
|
1623
1824
|
|
|
1624
|
-
|
|
1625
|
-
|
|
1825
|
+
const membership = new Membership({
|
|
1826
|
+
signaling,
|
|
1827
|
+
ctx: roomCtx,
|
|
1828
|
+
memberId,
|
|
1829
|
+
getToken,
|
|
1830
|
+
getTokenExpiresAt,
|
|
1831
|
+
isHost: () => Boolean(hostRole.controller),
|
|
1832
|
+
config: lobbyConfig,
|
|
1833
|
+
});
|
|
1626
1834
|
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
}
|
|
1835
|
+
const standby = new StandbyRole({
|
|
1836
|
+
signaling,
|
|
1837
|
+
ctx: roomCtx,
|
|
1838
|
+
isHost: () => Boolean(hostRole.controller),
|
|
1839
|
+
prepareRoom: hostRoomPrep.prepareHostRoom,
|
|
1840
|
+
diagnostics,
|
|
1841
|
+
config: lobbyConfig.migration,
|
|
1842
|
+
debug: isDevBuild ? debugLog : null,
|
|
1843
|
+
});
|
|
1637
1844
|
|
|
1638
|
-
|
|
1845
|
+
const hostRole = new HostRole({
|
|
1846
|
+
signaling,
|
|
1847
|
+
ctx: roomCtx,
|
|
1848
|
+
membership,
|
|
1849
|
+
config: lobbyConfig,
|
|
1850
|
+
diagnostics,
|
|
1851
|
+
prep: hostRoomPrep,
|
|
1852
|
+
getActiveGame: () => activeGameManifest,
|
|
1853
|
+
getToken,
|
|
1854
|
+
getHandoff: () => handoff,
|
|
1855
|
+
onPromotionRegistered: hostPromotion => promotionFlow.finish(hostPromotion),
|
|
1856
|
+
isLobbyMode,
|
|
1857
|
+
debug: isDevBuild ? debugLog : null,
|
|
1858
|
+
ui: {
|
|
1859
|
+
showRoomLink,
|
|
1860
|
+
refreshRoomControls,
|
|
1861
|
+
setHandoffMenu: state => roomMenu?.setHandoff(state),
|
|
1862
|
+
isHidden: isPageHidden,
|
|
1863
|
+
informTech,
|
|
1864
|
+
ensureWebRtc: ensureWebRtcAvailable,
|
|
1865
|
+
startSession: transport => startSession(transport),
|
|
1866
|
+
closeLobby: () => lobby?.close(),
|
|
1867
|
+
// Worker не поднялся (WASM/конфиг): гасим комнату и возвращаемся в лобби
|
|
1868
|
+
onStartFailed: msg => {
|
|
1869
|
+
roomCtx.startFailed = true;
|
|
1870
|
+
handleDisconnect();
|
|
1871
|
+
informTech(
|
|
1872
|
+
`Failed to start the room: ${msg.message || 'unknown error'}. Click to return to the lobby.`,
|
|
1873
|
+
);
|
|
1874
|
+
},
|
|
1875
|
+
},
|
|
1876
|
+
});
|
|
1639
1877
|
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1878
|
+
const handoff = new HandoffFlow({
|
|
1879
|
+
signaling,
|
|
1880
|
+
ctx: roomCtx,
|
|
1881
|
+
hostRole,
|
|
1882
|
+
standby,
|
|
1883
|
+
membership,
|
|
1884
|
+
getPromotion: () => promotionFlow,
|
|
1885
|
+
getSupervisor,
|
|
1886
|
+
getTokenExpiresAt,
|
|
1887
|
+
config: lobbyConfig.migration,
|
|
1888
|
+
ui: {
|
|
1889
|
+
showSessionOverlay,
|
|
1890
|
+
disableControls: () => modules.controls?.disableKeys(),
|
|
1891
|
+
enableControls: () => modules.controls?.enableKeys(),
|
|
1892
|
+
mute: () => soundManager.mute(),
|
|
1893
|
+
unmute: () => soundManager.unmute(),
|
|
1894
|
+
isHidden: isPageHidden,
|
|
1895
|
+
setHandoffMenu: state => roomMenu?.setHandoff(state),
|
|
1896
|
+
sendLeave: () => sending(PC_LEAVE),
|
|
1897
|
+
reloadPage,
|
|
1898
|
+
},
|
|
1899
|
+
});
|
|
1645
1900
|
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1901
|
+
const guest = new GuestSession({
|
|
1902
|
+
signaling,
|
|
1903
|
+
ctx: roomCtx,
|
|
1904
|
+
membership,
|
|
1905
|
+
hostRole,
|
|
1906
|
+
standby,
|
|
1907
|
+
getPromotion: () => promotionFlow,
|
|
1908
|
+
getSupervisor,
|
|
1909
|
+
getToken,
|
|
1910
|
+
logout: () => lobbyAuthModel.logout(),
|
|
1911
|
+
config: lobbyConfig,
|
|
1912
|
+
diagnostics,
|
|
1913
|
+
ui: {
|
|
1914
|
+
ensureWebRtc: ensureWebRtcAvailable,
|
|
1915
|
+
showRoomLink: roomId => showRoomLink(activeGameManifest?.id, roomId),
|
|
1916
|
+
startSession,
|
|
1917
|
+
closeLobby: () => lobby?.close(),
|
|
1918
|
+
leaveRoomWith,
|
|
1919
|
+
chat: chatNotice,
|
|
1920
|
+
getVote: () => modules.vote,
|
|
1921
|
+
},
|
|
1922
|
+
});
|
|
1654
1923
|
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1924
|
+
const promotionFlow = new PromotionFlow({
|
|
1925
|
+
signaling,
|
|
1926
|
+
ctx: roomCtx,
|
|
1927
|
+
hostRole,
|
|
1928
|
+
standby,
|
|
1929
|
+
membership,
|
|
1930
|
+
getHandoff: () => handoff,
|
|
1931
|
+
getGuest: () => guest,
|
|
1932
|
+
getSupervisor,
|
|
1933
|
+
getActiveGameId: () => activeGameManifest?.id,
|
|
1934
|
+
getToken,
|
|
1935
|
+
prepareRoom: hostRoomPrep.prepareHostRoom,
|
|
1936
|
+
config: lobbyConfig,
|
|
1937
|
+
diagnostics,
|
|
1938
|
+
ui: {
|
|
1939
|
+
reloadPage,
|
|
1940
|
+
reloadToRoom,
|
|
1941
|
+
showSessionOverlay,
|
|
1942
|
+
disableControls: () => modules.controls?.disableKeys(),
|
|
1943
|
+
mute: () => soundManager.mute(),
|
|
1944
|
+
informTech,
|
|
1945
|
+
chat: chatNotice,
|
|
1946
|
+
},
|
|
1947
|
+
});
|
|
1679
1948
|
|
|
1680
|
-
|
|
1681
|
-
|
|
1949
|
+
if (isLobbyMode) {
|
|
1950
|
+
const roomMenuModel = new RoomMenuModel();
|
|
1682
1951
|
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1952
|
+
roomMenu = new RoomMenuCtrl(
|
|
1953
|
+
roomMenuModel,
|
|
1954
|
+
new RoomMenuView(roomMenuModel, lobbyConfig.roomMenu),
|
|
1955
|
+
{
|
|
1956
|
+
onLeave: () => handoff.leaveByUser(),
|
|
1957
|
+
onHandover: () => handoff.start({ reason: 'handover', stay: true }),
|
|
1687
1958
|
},
|
|
1959
|
+
);
|
|
1688
1960
|
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1961
|
+
unloadGuard = new HostUnloadGuard({
|
|
1962
|
+
// мастер начинает аварийную миграцию сразу, не дожидаясь обрыва WS
|
|
1963
|
+
onHostLeave: () => {
|
|
1964
|
+
if (hostRole.room) {
|
|
1965
|
+
signaling.hostLeaving(hostRole.room.roomId, hostRole.room.epoch);
|
|
1966
|
+
}
|
|
1695
1967
|
},
|
|
1968
|
+
// место освобождается сразу, а не через resumeGraceMs
|
|
1969
|
+
onGuestLeave: () => handoff.announceGuestLeave(),
|
|
1696
1970
|
});
|
|
1971
|
+
}
|
|
1697
1972
|
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1973
|
+
// роль вкладки в комнате для меню и защиты закрытия: хост — пока у неё
|
|
1974
|
+
// Worker комнаты (и во время промоушена), гость — пока она в комнате
|
|
1975
|
+
function refreshRoomControls() {
|
|
1976
|
+
const role = !roomCtx.roomId ? null : hostRole.controller ? 'host' : 'guest';
|
|
1977
|
+
const othersPresent = role === 'host' && hostRole.peerCount > 0;
|
|
1978
|
+
|
|
1979
|
+
roomMenu?.setRole({
|
|
1980
|
+
role,
|
|
1981
|
+
othersPresent,
|
|
1982
|
+
hasSuccessor: role === 'host' && hostRole.successorMemberId !== null,
|
|
1702
1983
|
});
|
|
1984
|
+
unloadGuard?.update({ role, othersPresent });
|
|
1985
|
+
}
|
|
1703
1986
|
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1987
|
+
// адресная строка показывает ссылку на комнату, пока вкладка в ней: её
|
|
1988
|
+
// можно скопировать прямо оттуда, как и пунктом меню комнаты
|
|
1989
|
+
function showRoomLink(gameId, roomId) {
|
|
1990
|
+
if (!gameId) {
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1709
1993
|
|
|
1710
|
-
|
|
1711
|
-
transport.publisher.on('close', handleDisconnect);
|
|
1712
|
-
transport.connect();
|
|
1994
|
+
const link = formatRoomLink(gameId, roomId);
|
|
1713
1995
|
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1996
|
+
setRoute(link);
|
|
1997
|
+
roomMenu?.setLink(absoluteLink(link));
|
|
1998
|
+
refreshRoomControls();
|
|
1999
|
+
}
|
|
1717
2000
|
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
return; // комната уже погашена
|
|
1721
|
-
}
|
|
2001
|
+
// средний FPS рендера гостя за интервал отчёта (этап 9c)
|
|
2002
|
+
const fpsMeter = new FpsMeter();
|
|
1722
2003
|
|
|
1723
|
-
|
|
1724
|
-
|
|
2004
|
+
document.addEventListener('visibilitychange', () => {
|
|
2005
|
+
const hidden = isPageHidden();
|
|
1725
2006
|
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
});
|
|
2007
|
+
hostRole.setHidden(hidden);
|
|
2008
|
+
handoff.setHidden(hidden);
|
|
1729
2009
|
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
}
|
|
2010
|
+
// кадры скрытой вкладки (их нет) не тянут средний FPS вниз
|
|
2011
|
+
if (!hidden) {
|
|
2012
|
+
fpsMeter.reset();
|
|
2013
|
+
}
|
|
2014
|
+
});
|
|
1734
2015
|
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
// доказывает мастеру владение hostId)
|
|
1743
|
-
hostController?.setHostId(msg.hostId, msg.hostSecret);
|
|
1744
|
-
|
|
1745
|
-
if (msg.mapsVersion && msg.mapsVersion !== hostMapsVersion) {
|
|
1746
|
-
refreshHostMaps();
|
|
1747
|
-
}
|
|
2016
|
+
// закрытие скрытой вкладки: эпизод закрывается сводкой сейчас — журнал свой
|
|
2017
|
+
// pagehide уже отработал (подписан раньше), поэтому отправка явная
|
|
2018
|
+
window.addEventListener('pagehide', () => {
|
|
2019
|
+
if (hostRole.flushHiddenLog()) {
|
|
2020
|
+
diagnostics.flush();
|
|
2021
|
+
}
|
|
2022
|
+
});
|
|
1748
2023
|
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
2024
|
+
// solo: авторитетный хост в главном потоке (без Worker'а)
|
|
2025
|
+
let inlineHost = null;
|
|
2026
|
+
|
|
2027
|
+
if (isDevBuild) {
|
|
2028
|
+
window.__vimpDebug = createDebugApi({
|
|
2029
|
+
getHostController: () => hostRole.controller,
|
|
2030
|
+
getClientCore: () => clientCore,
|
|
2031
|
+
reportUrl: lobbyConfig.debugReportUrl,
|
|
2032
|
+
startHandoff: options => handoff.start(options),
|
|
1756
2033
|
});
|
|
1757
2034
|
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
}
|
|
2035
|
+
debugLog(
|
|
2036
|
+
'window.__vimpDebug is available: dump, startRecording, stopRecording, divergence',
|
|
2037
|
+
);
|
|
2038
|
+
}
|
|
1763
2039
|
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
2040
|
+
// WebRTC обязателен для P2P-игры. В Firefox RTCPeerConnection может
|
|
2041
|
+
// отсутствовать (media.peerconnection.enabled = false, resistFingerprinting,
|
|
2042
|
+
// приватные сборки) — честное сообщение вместо падения с чёрным экраном
|
|
2043
|
+
function ensureWebRtcAvailable() {
|
|
2044
|
+
if (typeof RTCPeerConnection !== 'undefined') {
|
|
2045
|
+
return true;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
socketMethods[PS_TECH_INFORM_DATA](
|
|
2049
|
+
'WebRTC is unavailable in this browser: P2P play is impossible. ' +
|
|
2050
|
+
'In Firefox check that media.peerconnection.enabled is on.',
|
|
2051
|
+
);
|
|
1772
2052
|
|
|
1773
|
-
|
|
2053
|
+
return false;
|
|
1774
2054
|
}
|
|
1775
2055
|
|
|
1776
2056
|
// solo-режим: авторитетный матч в этом же потоке (standalone SDK). Ни
|
|
@@ -1796,163 +2076,20 @@ async function connectSolo() {
|
|
|
1796
2076
|
// хендшейк начинается в connect() — хост к этому моменту обязан быть готов
|
|
1797
2077
|
await inlineHost.ready;
|
|
1798
2078
|
|
|
1799
|
-
transport = new LoopbackTransport(inlineHost, socketId);
|
|
2079
|
+
const transport = new LoopbackTransport(inlineHost, socketId);
|
|
1800
2080
|
|
|
1801
|
-
transport
|
|
1802
|
-
transport.publisher.on('close', handleDisconnect);
|
|
2081
|
+
startSession(transport);
|
|
1803
2082
|
transport.connect();
|
|
1804
2083
|
}
|
|
1805
2084
|
|
|
1806
2085
|
// dedicated-режим: прямой WebSocket к Node-серверу игры
|
|
1807
2086
|
function connectDedicated() {
|
|
1808
|
-
transport = new WebSocketTransport(boot.wsUrl);
|
|
2087
|
+
const transport = new WebSocketTransport(boot.wsUrl);
|
|
1809
2088
|
|
|
1810
|
-
transport
|
|
1811
|
-
transport.publisher.on('close', handleDisconnect);
|
|
2089
|
+
startSession(transport);
|
|
1812
2090
|
transport.connect();
|
|
1813
2091
|
}
|
|
1814
2092
|
|
|
1815
|
-
// версия каталога карт мастера, с которой поднята комната (Этап 5.1)
|
|
1816
|
-
let hostMapsVersion = null;
|
|
1817
|
-
|
|
1818
|
-
// составной codeVersion комнаты (Этап 5.2/6.5): { engine, game: { id, version } };
|
|
1819
|
-
// null — обновления кода отключены (манифест недоступен при старте)
|
|
1820
|
-
let hostCodeVersion = null;
|
|
1821
|
-
|
|
1822
|
-
// версия, своп на которую не удался — не ретраить её на каждом re-register
|
|
1823
|
-
let failedCodeVersion = null;
|
|
1824
|
-
|
|
1825
|
-
// сравнимый ключ составного codeVersion (Этап 6.5): движок + игра —
|
|
1826
|
-
// расхождение любой половины (деплой движка ИЛИ деплой игры) запускает эстафету
|
|
1827
|
-
function codeVersionKey(cv) {
|
|
1828
|
-
return cv
|
|
1829
|
-
? `${cv.engine ?? ''}:${cv.game?.id ?? ''}:${cv.game?.version ?? ''}`
|
|
1830
|
-
: null;
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
function codeVersionChanged(remote, local) {
|
|
1834
|
-
return codeVersionKey(remote) !== codeVersionKey(local);
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
// защита от параллельных эстафет Worker'ов
|
|
1838
|
-
let workerSwapInProgress = false;
|
|
1839
|
-
|
|
1840
|
-
// повторная регистрация комнаты у мастера (reconnect сигналинга)
|
|
1841
|
-
let hostRegistration = null;
|
|
1842
|
-
|
|
1843
|
-
// Этап 5.1/6.4: скачивает каталог карт мастера активной игры (манифест +
|
|
1844
|
-
// все карты)
|
|
1845
|
-
async function fetchMasterMaps() {
|
|
1846
|
-
const manifestRes = await fetch(lobbyConfig.maps.manifestUrl(activeGameManifest));
|
|
1847
|
-
|
|
1848
|
-
if (!manifestRes.ok) {
|
|
1849
|
-
throw new Error(`maps manifest: HTTP ${manifestRes.status}`);
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
const manifest = await manifestRes.json();
|
|
1853
|
-
|
|
1854
|
-
const entries = await Promise.all(
|
|
1855
|
-
manifest.maps.map(async name => {
|
|
1856
|
-
const url = `${lobbyConfig.maps.baseUrl(activeGameManifest)}/${encodeURIComponent(name)}`;
|
|
1857
|
-
const res = await fetch(url);
|
|
1858
|
-
|
|
1859
|
-
if (!res.ok) {
|
|
1860
|
-
throw new Error(`map ${name}: HTTP ${res.status}`);
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
return [name, await res.json()];
|
|
1864
|
-
}),
|
|
1865
|
-
);
|
|
1866
|
-
|
|
1867
|
-
return { version: manifest.version, maps: Object.fromEntries(entries) };
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
// перечитывает каталог карт мастера и передаёт в Worker:
|
|
1871
|
-
// применится со следующей смены карты (текущий раунд не трогается)
|
|
1872
|
-
async function refreshHostMaps() {
|
|
1873
|
-
try {
|
|
1874
|
-
const catalog = await fetchMasterMaps();
|
|
1875
|
-
|
|
1876
|
-
hostMapsVersion = catalog.version;
|
|
1877
|
-
hostController?.updateMaps(catalog.maps);
|
|
1878
|
-
} catch (e) {
|
|
1879
|
-
console.warn('[maps] refresh from master failed:', e);
|
|
1880
|
-
}
|
|
1881
|
-
}
|
|
1882
|
-
|
|
1883
|
-
// Этап 5.2: скачивает манифест worker-бандла мастера ({ version, url })
|
|
1884
|
-
async function fetchWorkerManifest() {
|
|
1885
|
-
const res = await fetch(lobbyConfig.worker.manifestUrl);
|
|
1886
|
-
|
|
1887
|
-
if (!res.ok) {
|
|
1888
|
-
throw new Error(`worker manifest: HTTP ${res.status}`);
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
return res.json();
|
|
1892
|
-
}
|
|
1893
|
-
|
|
1894
|
-
// Этап 6.5: перечитывает манифест активной игры мастера — своп не должен
|
|
1895
|
-
// нести новому Worker'у закэшированный с момента создания комнаты
|
|
1896
|
-
// hostEntryUrl/wasmUrl (деплой игры мог обновиться независимо от движка)
|
|
1897
|
-
async function fetchGameManifest(gameId) {
|
|
1898
|
-
return fetchGamePluginManifest(lobbyConfig.game.manifestUrl(gameId));
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
// Этап 5.2/6.5: эстафета Worker'ов — новая версия кода (движка ИЛИ игры) у
|
|
1902
|
-
// мастера. Worker заменяется на границе раунда без разрыва P2P; сбой свопа
|
|
1903
|
-
// не смертелен — комната продолжает жить на прежней версии
|
|
1904
|
-
async function refreshHostWorker() {
|
|
1905
|
-
if (workerSwapInProgress || !hostController) {
|
|
1906
|
-
return;
|
|
1907
|
-
}
|
|
1908
|
-
|
|
1909
|
-
workerSwapInProgress = true;
|
|
1910
|
-
|
|
1911
|
-
let manifest = null;
|
|
1912
|
-
let game = null;
|
|
1913
|
-
|
|
1914
|
-
try {
|
|
1915
|
-
manifest = await fetchWorkerManifest();
|
|
1916
|
-
const gameManifest = await fetchGameManifest(activeGameManifest.id);
|
|
1917
|
-
|
|
1918
|
-
game = {
|
|
1919
|
-
id: gameManifest.id,
|
|
1920
|
-
version: gameManifest.version,
|
|
1921
|
-
hostEntryUrl: gameManifest.entries.host,
|
|
1922
|
-
wasmUrl: gameManifest.entries.wasm,
|
|
1923
|
-
};
|
|
1924
|
-
|
|
1925
|
-
const nextCodeVersion = {
|
|
1926
|
-
engine: manifest.version,
|
|
1927
|
-
game: { id: game.id, version: game.version },
|
|
1928
|
-
};
|
|
1929
|
-
const nextKey = codeVersionKey(nextCodeVersion);
|
|
1930
|
-
|
|
1931
|
-
if (
|
|
1932
|
-
!manifest.version ||
|
|
1933
|
-
!manifest.url ||
|
|
1934
|
-
nextKey === codeVersionKey(hostCodeVersion) ||
|
|
1935
|
-
nextKey === codeVersionKey(failedCodeVersion)
|
|
1936
|
-
) {
|
|
1937
|
-
return;
|
|
1938
|
-
}
|
|
1939
|
-
|
|
1940
|
-
await hostController.swapWorker(manifest.url, game);
|
|
1941
|
-
|
|
1942
|
-
hostCodeVersion = nextCodeVersion;
|
|
1943
|
-
failedCodeVersion = null;
|
|
1944
|
-
console.info(`[worker] room migrated to code version ${nextKey}`);
|
|
1945
|
-
} catch (e) {
|
|
1946
|
-
if (manifest?.version) {
|
|
1947
|
-
failedCodeVersion = { engine: manifest.version, game };
|
|
1948
|
-
}
|
|
1949
|
-
|
|
1950
|
-
console.warn('[worker] swap to new version failed:', e);
|
|
1951
|
-
} finally {
|
|
1952
|
-
workerSwapInProgress = false;
|
|
1953
|
-
}
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
2093
|
// REST-запрос списка серверов у мастера (поиск игнорирует пагинацию)
|
|
1957
2094
|
async function fetchServers({ offset, limit, search }) {
|
|
1958
2095
|
const params = new URLSearchParams();
|
|
@@ -2102,8 +2239,9 @@ function clearLobbyError() {
|
|
|
2102
2239
|
}
|
|
2103
2240
|
|
|
2104
2241
|
// активирует игру и перепривязывает манифест/плагин/CSS; false — отказ уже
|
|
2105
|
-
// показан
|
|
2106
|
-
|
|
2242
|
+
// показан игроку (report, по умолчанию строкой формы лобби), лобби остаётся
|
|
2243
|
+
// рабочим
|
|
2244
|
+
async function selectActiveGame(gameId, { report = showLobbyError } = {}) {
|
|
2107
2245
|
// сверка идёт с самим манифестом, а не с его id: каталог вкладки может
|
|
2108
2246
|
// пополниться застейдженной версией той же игры (registerGameManifest), и
|
|
2109
2247
|
// сравнение по id оставило бы активной одобренную версию
|
|
@@ -2125,7 +2263,7 @@ async function selectActiveGame(gameId) {
|
|
|
2125
2263
|
return true;
|
|
2126
2264
|
} catch (e) {
|
|
2127
2265
|
console.error(`[game] failed to activate "${gameId}":`, e);
|
|
2128
|
-
|
|
2266
|
+
report(
|
|
2129
2267
|
`Failed to load ${gamesById.get(gameId)?.title ?? gameId}: ${e.message}`,
|
|
2130
2268
|
);
|
|
2131
2269
|
|
|
@@ -2146,7 +2284,7 @@ const stagedGameIds = new Set();
|
|
|
2146
2284
|
* так админ может поднять по нему комнату, не трогая каталог игроков.
|
|
2147
2285
|
* Дальше всё работает существующим путём: createGameActivator грузит
|
|
2148
2286
|
* ClientPlugin по entries.client (ключ кеша версионный, поэтому плагин
|
|
2149
|
-
* черновика не подменит одобренный),
|
|
2287
|
+
* черновика не подменит одобренный), hostRole.createRoom поднимает комнату, а
|
|
2150
2288
|
* мастер помечает её скрытой по версии манифеста.
|
|
2151
2289
|
* @param {Object} manifest - Манифест застейдженной версии из /admin/games.
|
|
2152
2290
|
* @returns {void}
|
|
@@ -2251,8 +2389,8 @@ function initLobby() {
|
|
|
2251
2389
|
});
|
|
2252
2390
|
|
|
2253
2391
|
// умный пинг видимого сервера — сигнальным путём (замер приблизительный)
|
|
2254
|
-
lobbyModel.publisher.on('ping-request', ({
|
|
2255
|
-
signaling.pingHost(
|
|
2392
|
+
lobbyModel.publisher.on('ping-request', ({ roomId, pingId }) => {
|
|
2393
|
+
signaling.pingHost(roomId, pingId);
|
|
2256
2394
|
});
|
|
2257
2395
|
|
|
2258
2396
|
signaling.publisher.on('pong_host', msg => {
|
|
@@ -2262,7 +2400,7 @@ function initLobby() {
|
|
|
2262
2400
|
// выбор сервера → активация игры комнаты и установка P2P. gameId нет у
|
|
2263
2401
|
// хостов старше 6.4 — тогда заходим на активной игре, как раньше: неизвестная
|
|
2264
2402
|
// версия не повод запретить вход
|
|
2265
|
-
lobbyModel.publisher.on('join', async ({
|
|
2403
|
+
lobbyModel.publisher.on('join', async ({ roomId, gameId }) => {
|
|
2266
2404
|
if (gameId && gamesById.has(gameId)) {
|
|
2267
2405
|
if (!(await selectActiveGame(gameId))) {
|
|
2268
2406
|
return;
|
|
@@ -2275,7 +2413,7 @@ function initLobby() {
|
|
|
2275
2413
|
return;
|
|
2276
2414
|
}
|
|
2277
2415
|
|
|
2278
|
-
|
|
2416
|
+
guest.connectToRoom(roomId);
|
|
2279
2417
|
});
|
|
2280
2418
|
|
|
2281
2419
|
// Leaderboard (lobby-page-plan): контроллер сигнализирует, для какой игры
|
|
@@ -2324,7 +2462,9 @@ function initLobby() {
|
|
|
2324
2462
|
// реестр игр (master-game-registry, этап 4): заявка разработчика и панель
|
|
2325
2463
|
// модерации живут в том же лобби. Триплет поднимается вместе с лобби —
|
|
2326
2464
|
// панель доступна любому авторизованному, кнопку модерации показывает роль
|
|
2327
|
-
const gamesModel = new GamesModel(lobbyConfig.games, () =>
|
|
2465
|
+
const gamesModel = new GamesModel(lobbyConfig.games, () =>
|
|
2466
|
+
lobbyAuthModel.getToken(),
|
|
2467
|
+
);
|
|
2328
2468
|
const gamesView = new GamesView(gamesModel, lobbyConfig.games);
|
|
2329
2469
|
const games = new GamesCtrl(gamesModel, gamesView);
|
|
2330
2470
|
|
|
@@ -2335,11 +2475,18 @@ function initLobby() {
|
|
|
2335
2475
|
|
|
2336
2476
|
// журнал клиентских ошибок (plan/client-reports, этап 5): кнопку «Errors»
|
|
2337
2477
|
// видит только админ; фильтр по игре — каталог вкладки
|
|
2338
|
-
const clientReportsModel = new ClientReportsModel(
|
|
2339
|
-
|
|
2478
|
+
const clientReportsModel = new ClientReportsModel(
|
|
2479
|
+
lobbyConfig.clientReports,
|
|
2480
|
+
() => lobbyAuthModel.getToken(),
|
|
2481
|
+
);
|
|
2482
|
+
const clientReportsView = new ClientReportsView(
|
|
2483
|
+
clientReportsModel,
|
|
2484
|
+
lobbyConfig.clientReports,
|
|
2485
|
+
);
|
|
2486
|
+
const clientReports = new ClientReportsCtrl(
|
|
2487
|
+
clientReportsModel,
|
|
2488
|
+
clientReportsView,
|
|
2340
2489
|
);
|
|
2341
|
-
const clientReportsView = new ClientReportsView(clientReportsModel, lobbyConfig.clientReports);
|
|
2342
|
-
const clientReports = new ClientReportsCtrl(clientReportsModel, clientReportsView);
|
|
2343
2490
|
|
|
2344
2491
|
clientReportsView.setGames([...gamesById.values()]);
|
|
2345
2492
|
clientReports.setAdmin(lobbyAuthModel.getRole() === 'admin');
|
|
@@ -2361,22 +2508,24 @@ function initLobby() {
|
|
|
2361
2508
|
// мастера синхронным циклом, и без очереди два черновика при пустом
|
|
2362
2509
|
// каталоге ушли бы активироваться одновременно — активной осталась бы
|
|
2363
2510
|
// одна игра, а форма комнаты собралась бы по другой
|
|
2364
|
-
stagedActivation = stagedActivation
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2511
|
+
stagedActivation = stagedActivation
|
|
2512
|
+
.then(async () => {
|
|
2513
|
+
if (activeGameManifest) {
|
|
2514
|
+
return; // предыдущий черновик уже стал активным
|
|
2515
|
+
}
|
|
2368
2516
|
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2517
|
+
// только на успехе: отказ активации уже написал в ту же строку СВОЮ
|
|
2518
|
+
// причину («Failed to load X: …»), и общее «игр пока не опубликовано»
|
|
2519
|
+
// затёрло бы её — админ увидел бы не то, что случилось
|
|
2520
|
+
if (await selectActiveGame(manifest.id)) {
|
|
2521
|
+
syncCatalogState();
|
|
2522
|
+
}
|
|
2523
|
+
})
|
|
2524
|
+
.catch(e => {
|
|
2525
|
+
// отказ не должен заклинить очередь: следующий черновик обязан
|
|
2526
|
+
// получить свой шанс стать активной игрой
|
|
2527
|
+
console.error('[game] staged activation failed:', e);
|
|
2528
|
+
});
|
|
2380
2529
|
});
|
|
2381
2530
|
|
|
2382
2531
|
// создание комнаты в этой же вкладке (хост-игрок через loopback)
|
|
@@ -2402,8 +2551,6 @@ function initLobby() {
|
|
|
2402
2551
|
liveErrors.disarm();
|
|
2403
2552
|
});
|
|
2404
2553
|
|
|
2405
|
-
const nameInput = document.getElementById(lobbyConfig.elems.nameId);
|
|
2406
|
-
|
|
2407
2554
|
hostBtn?.addEventListener('click', async () => {
|
|
2408
2555
|
// валидация (pattern/required/min/max) — единственная граница
|
|
2409
2556
|
// room-формы: она едет клиенту как JSON манифеста, JS-валидаторы
|
|
@@ -2419,8 +2566,6 @@ function initLobby() {
|
|
|
2419
2566
|
}
|
|
2420
2567
|
|
|
2421
2568
|
const manifest = gamesById.get(gameSelect?.value) ?? activeGameManifest;
|
|
2422
|
-
const name =
|
|
2423
|
-
(nameInput?.value || '').trim() || lobbyConfig.create.defaultName;
|
|
2424
2569
|
|
|
2425
2570
|
// значения формы снимаем до await: активация асинхронна, а roomFormFields
|
|
2426
2571
|
// пересобираются при смене игры — в комнату должно уехать то, что игрок
|
|
@@ -2441,8 +2586,7 @@ function initLobby() {
|
|
|
2441
2586
|
return;
|
|
2442
2587
|
}
|
|
2443
2588
|
|
|
2444
|
-
|
|
2445
|
-
name,
|
|
2589
|
+
hostRole.createRoom({
|
|
2446
2590
|
hostSocketId: lobbyConfig.create.hostSocketId,
|
|
2447
2591
|
...overrides,
|
|
2448
2592
|
});
|
|
@@ -2461,9 +2605,52 @@ let lobbyAuthView = null;
|
|
|
2461
2605
|
let welcomeReceived = false;
|
|
2462
2606
|
let authenticated = false;
|
|
2463
2607
|
|
|
2608
|
+
// прямые ссылки и быстрая игра (client/session/RouteBoot.js)
|
|
2609
|
+
const route = new RouteBoot({
|
|
2610
|
+
ctx: roomCtx,
|
|
2611
|
+
signaling,
|
|
2612
|
+
membership,
|
|
2613
|
+
config: lobbyConfig,
|
|
2614
|
+
gamesById,
|
|
2615
|
+
selectActiveGame,
|
|
2616
|
+
connectToRoom: roomId => guest.connectToRoom(roomId),
|
|
2617
|
+
createRoom: (room, options) => hostRole.createRoom(room, options),
|
|
2618
|
+
fetchServers,
|
|
2619
|
+
ui: {
|
|
2620
|
+
initLobby,
|
|
2621
|
+
selectLobbyGame: gameId => {
|
|
2622
|
+
const gameSelect = document.getElementById(lobbyConfig.elems.gameId);
|
|
2623
|
+
|
|
2624
|
+
if (gameSelect) {
|
|
2625
|
+
gameSelect.value = gameId;
|
|
2626
|
+
gameSelect.dispatchEvent(new Event('change'));
|
|
2627
|
+
}
|
|
2628
|
+
},
|
|
2629
|
+
informTech,
|
|
2630
|
+
onInformerClick: handler =>
|
|
2631
|
+
techInformer.addEventListener('click', handler, { once: true }),
|
|
2632
|
+
getHash: () => location.hash,
|
|
2633
|
+
setRoute,
|
|
2634
|
+
reloadPage,
|
|
2635
|
+
},
|
|
2636
|
+
});
|
|
2637
|
+
|
|
2464
2638
|
function maybeInitLobby() {
|
|
2465
2639
|
if (welcomeReceived && authenticated) {
|
|
2466
|
-
|
|
2640
|
+
route.boot();
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
// показывает терминальную причину и возвращает в лобби (путь
|
|
2645
|
+
// handleDisconnect: закрытие транспорта → перезагрузка)
|
|
2646
|
+
function leaveRoomWith(message) {
|
|
2647
|
+
socketMethods[PS_TECH_INFORM_DATA](message);
|
|
2648
|
+
terminalInformShown = true;
|
|
2649
|
+
|
|
2650
|
+
if (supervisor) {
|
|
2651
|
+
supervisor.close();
|
|
2652
|
+
} else {
|
|
2653
|
+
handleDisconnect();
|
|
2467
2654
|
}
|
|
2468
2655
|
}
|
|
2469
2656
|
|
|
@@ -2480,15 +2667,51 @@ if (isLobbyMode) {
|
|
|
2480
2667
|
maybeInitLobby();
|
|
2481
2668
|
});
|
|
2482
2669
|
|
|
2670
|
+
guest.bind();
|
|
2671
|
+
hostRole.bind();
|
|
2672
|
+
standby.bind();
|
|
2673
|
+
promotionFlow.bind();
|
|
2674
|
+
handoff.bind();
|
|
2675
|
+
|
|
2676
|
+
document.addEventListener('visibilitychange', () => membership.sendUpdate());
|
|
2677
|
+
|
|
2678
|
+
// свежий средний FPS гостя мастеру (этап 9c); хост его не шлёт
|
|
2679
|
+
Ticker.shared.add(() => fpsMeter.frame());
|
|
2680
|
+
setInterval(
|
|
2681
|
+
() => membership.setFps(fpsMeter.sample()),
|
|
2682
|
+
lobbyConfig.migration.auto.fpsReportIntervalMs,
|
|
2683
|
+
);
|
|
2684
|
+
|
|
2483
2685
|
lobbyAuthModel.publisher.on('authenticated', () => {
|
|
2484
2686
|
authenticated = true;
|
|
2485
2687
|
maybeInitLobby();
|
|
2688
|
+
// новый вход — новый срок: canHost пересчитается по нему, передача роли
|
|
2689
|
+
// хоста — тоже
|
|
2690
|
+
membership.armTokenCapsTimer();
|
|
2691
|
+
|
|
2692
|
+
if (hostRole.controller) {
|
|
2693
|
+
handoff.armTokenHandoff();
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
// повторный вход после logout: маршрут уже разобран, а логин-гейт
|
|
2697
|
+
// спрятал лобби — вернуть его, если вкладка не в комнате
|
|
2698
|
+
if (route.booted && !roomCtx.entered) {
|
|
2699
|
+
lobby?.open();
|
|
2700
|
+
}
|
|
2486
2701
|
});
|
|
2487
2702
|
|
|
2703
|
+
// OAuth-возврат: токены из query прочитаны — чистим только query, hash
|
|
2704
|
+
// (маршрут #/<gameId>[/<roomId>]) остаётся
|
|
2488
2705
|
if (lobbyAuthCtrl.init(window.location.search)) {
|
|
2489
|
-
window.history.replaceState(
|
|
2706
|
+
window.history.replaceState(
|
|
2707
|
+
null,
|
|
2708
|
+
'',
|
|
2709
|
+
window.location.pathname + window.location.hash,
|
|
2710
|
+
);
|
|
2490
2711
|
}
|
|
2491
2712
|
|
|
2713
|
+
window.addEventListener('hashchange', () => route.handleHashChange());
|
|
2714
|
+
|
|
2492
2715
|
signaling.connect();
|
|
2493
2716
|
} else if (bootMode === 'solo') {
|
|
2494
2717
|
try {
|