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/bin/vimp-sim.js
CHANGED
|
@@ -26,6 +26,11 @@ const USAGE = `Usage: vimp-sim [options]
|
|
|
26
26
|
--out <dir> report root (default: .debug)
|
|
27
27
|
--no-write print the report to stdout instead of writing files
|
|
28
28
|
--determinism run the scenario twice and compare the frame streams
|
|
29
|
+
--checkpoint-every <ms>
|
|
30
|
+
every <ms> of match time take a host checkpoint, raise a
|
|
31
|
+
NEW host from it and resume every client (RESUME) —
|
|
32
|
+
checks the game survives a host change; overrides the
|
|
33
|
+
scenario's checkpointEvery
|
|
29
34
|
--help
|
|
30
35
|
`;
|
|
31
36
|
|
|
@@ -43,7 +48,7 @@ const BUILTIN_NOTICE =
|
|
|
43
48
|
// wasmUrl не смотрит, поэтому прогон был бы зелёным, не тронув ядро игры
|
|
44
49
|
const STRAY_CORE_NOTICE =
|
|
45
50
|
'notice: --core without --game does nothing: the run falls back to the\n' +
|
|
46
|
-
|
|
51
|
+
'notice: built-in fixture, whose core is plain JS. Add --game <path>.\n\n';
|
|
47
52
|
|
|
48
53
|
async function main(argv) {
|
|
49
54
|
const args = parseArgs(argv);
|
|
@@ -70,6 +75,18 @@ async function main(argv) {
|
|
|
70
75
|
const scenario = args.scenario
|
|
71
76
|
? JSON.parse(await readFile(args.scenario, 'utf8'))
|
|
72
77
|
: builtinScenario(plugin);
|
|
78
|
+
|
|
79
|
+
if (args['checkpoint-every'] !== undefined) {
|
|
80
|
+
const every = Number(args['checkpoint-every']);
|
|
81
|
+
|
|
82
|
+
if (!Number.isFinite(every) || every <= 0) {
|
|
83
|
+
throw new UsageError(
|
|
84
|
+
`option '--checkpoint-every' needs a positive number of ms\n\n${USAGE}`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
scenario.checkpointEvery = every;
|
|
89
|
+
}
|
|
73
90
|
// хеши потока кадров собираются только под --determinism: их единственный
|
|
74
91
|
// потребитель — сравнение двух прогонов, а объём линеен по длине матча
|
|
75
92
|
const captureFrames = args.determinism === true;
|
|
@@ -132,6 +149,7 @@ function parseArgs(argv) {
|
|
|
132
149
|
case '--game':
|
|
133
150
|
case '--core':
|
|
134
151
|
case '--out':
|
|
152
|
+
case '--checkpoint-every':
|
|
135
153
|
i += 1;
|
|
136
154
|
|
|
137
155
|
// «--game» последним аргументом (или перед следующим флагом) тихо
|
package/core/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "vimp-engine-core"
|
|
3
|
-
version = "0.23.
|
|
3
|
+
version = "0.23.2"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
description = "VIMP — движковый каркас симуляции (физика, карта, снапшот-фрейминг, интерполяция/предикт/raycast-примитивы, нав-утилиты). Без wasm-bindgen — обёртки для WASM-ABI собирает game-crate."
|
|
6
6
|
license = "MIT"
|
package/package.json
CHANGED
|
@@ -49,6 +49,11 @@ export default class ControlsCtrl {
|
|
|
49
49
|
this._model.setKeysEnabled(false);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// повторяет нажатия удерживаемых клавиш (возобновление сессии)
|
|
53
|
+
resendHeld() {
|
|
54
|
+
this._model.resendHeld();
|
|
55
|
+
}
|
|
56
|
+
|
|
52
57
|
// скрывает курсор
|
|
53
58
|
resetCursorHideTimer() {
|
|
54
59
|
this._view.resetCursorHideTimer();
|
|
@@ -64,12 +64,12 @@ export default class LobbyCtrl {
|
|
|
64
64
|
this._model.loadMore();
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
pingHost(
|
|
68
|
-
this._model.pingHost(
|
|
67
|
+
pingHost(roomId) {
|
|
68
|
+
this._model.pingHost(roomId, this._clock());
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
join(
|
|
72
|
-
this._model.join(
|
|
71
|
+
join(roomId) {
|
|
72
|
+
this._model.join(roomId);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
showTab(tab) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Singleton RoomMenuCtrl
|
|
2
|
+
|
|
3
|
+
let roomMenuCtrl;
|
|
4
|
+
|
|
5
|
+
export default class RoomMenuCtrl {
|
|
6
|
+
// actions — { onLeave, onHandover }: действия пунктов (client/main.js)
|
|
7
|
+
constructor(model, view, actions = {}) {
|
|
8
|
+
if (roomMenuCtrl) {
|
|
9
|
+
return roomMenuCtrl;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
roomMenuCtrl = this;
|
|
13
|
+
|
|
14
|
+
this._model = model;
|
|
15
|
+
this._view = view;
|
|
16
|
+
|
|
17
|
+
this._vPublic = view.publisher;
|
|
18
|
+
this._vPublic.on('toggle', () => model.toggle());
|
|
19
|
+
// меню остаётся открытым: на месте пунктов — статус передачи
|
|
20
|
+
this._vPublic.on('leave', () => actions.onLeave?.());
|
|
21
|
+
this._vPublic.on('handover', () => actions.onHandover?.());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// абсолютная ссылка на комнату, в которой вкладка; null — скрыть меню
|
|
25
|
+
setLink(link) {
|
|
26
|
+
this._model.setLink(link);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// { role, othersPresent, hasSuccessor } — см. RoomMenuModel.setRole
|
|
30
|
+
setRole(state) {
|
|
31
|
+
this._model.setRole(state);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// true — кнопка внутри панели (игра её показывает), false — в углу
|
|
35
|
+
setInPanel(inPanel) {
|
|
36
|
+
this._view.placeInPanel(inPanel);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// null | 'pending' | 'slow' | 'failed'
|
|
40
|
+
setHandoff(status) {
|
|
41
|
+
this._model.setHandoff(status);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -29,8 +29,10 @@ export default class VoteCtrl {
|
|
|
29
29
|
this._model.updateValues(data);
|
|
30
30
|
// если данные - объект (данные для создания голосования)
|
|
31
31
|
} else if (typeof data === 'object' && data !== null) {
|
|
32
|
-
|
|
33
|
-
this._model.
|
|
32
|
+
// false — ждёт закрытия открытого голосования движка
|
|
33
|
+
if (this._model.createWithTemplate(data) !== false) {
|
|
34
|
+
this._model.open();
|
|
35
|
+
}
|
|
34
36
|
// иначе открыть меню
|
|
35
37
|
} else {
|
|
36
38
|
this._model.createMenu();
|
|
@@ -52,4 +54,18 @@ export default class VoteCtrl {
|
|
|
52
54
|
removeVote() {
|
|
53
55
|
this._model.complete();
|
|
54
56
|
}
|
|
57
|
+
|
|
58
|
+
// голосование движка (host-migration этап 10: «Change host?» от мастера)
|
|
59
|
+
openEngineVote(vote) {
|
|
60
|
+
this._model.createEngineVote(vote);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
closeEngineVote(name) {
|
|
64
|
+
this._model.closeEngineVote(name);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// голосования хоста не пережили переподключение
|
|
68
|
+
removeHostVotes() {
|
|
69
|
+
this._model.removeHostVotes();
|
|
70
|
+
}
|
|
55
71
|
}
|
|
@@ -90,7 +90,9 @@ export default class ClientReportsModel {
|
|
|
90
90
|
this._error = null;
|
|
91
91
|
this._emit();
|
|
92
92
|
|
|
93
|
-
const { ok, json } = await this._request(
|
|
93
|
+
const { ok, json } = await this._request(
|
|
94
|
+
`${this._config.urls.list}?${query}`,
|
|
95
|
+
);
|
|
94
96
|
|
|
95
97
|
if (seq !== this._seq) {
|
|
96
98
|
return;
|
|
@@ -128,7 +130,9 @@ export default class ClientReportsModel {
|
|
|
128
130
|
this._error = null;
|
|
129
131
|
|
|
130
132
|
if (this._status === 'all' || report.status === this._status) {
|
|
131
|
-
this._items = this._items.map(item =>
|
|
133
|
+
this._items = this._items.map(item =>
|
|
134
|
+
item.id === report.id ? report : item,
|
|
135
|
+
);
|
|
132
136
|
} else {
|
|
133
137
|
const before = this._items.length;
|
|
134
138
|
|
|
@@ -20,6 +20,10 @@ export default class ControlsModel {
|
|
|
20
20
|
this._currentKeySet = this._keySetList[0]; // текущий набор клавиш
|
|
21
21
|
this._currentModes = {}; // статусы режимов
|
|
22
22
|
this._pressedKeys = {}; // объект для хранения состояния зажатых клавиш
|
|
23
|
+
// физически зажатые клавиши (keyCode) — независимо от набора и запрета
|
|
24
|
+
// ввода: _pressedKeys сбрасывает смена набора, а resendHeld() после
|
|
25
|
+
// возобновления сессии должен знать, что игрок держит на самом деле
|
|
26
|
+
this._heldCodes = new Set();
|
|
23
27
|
this._areKeysEnabled = false; // статус возможности нажатия клавиш
|
|
24
28
|
|
|
25
29
|
// канал указателя: игра, не объявившая modules.controls.pointer, его не
|
|
@@ -51,6 +55,8 @@ export default class ControlsModel {
|
|
|
51
55
|
const mode = this._modes[keyCode];
|
|
52
56
|
const cmd = this._cmds[keyCode];
|
|
53
57
|
|
|
58
|
+
this._heldCodes.add(keyCode);
|
|
59
|
+
|
|
54
60
|
// если запрет на ввод клавиш,
|
|
55
61
|
// то доступны только stat и chat
|
|
56
62
|
if (
|
|
@@ -103,6 +109,8 @@ export default class ControlsModel {
|
|
|
103
109
|
const keyCode = event.keyCode;
|
|
104
110
|
const mode = this._modes[keyCode];
|
|
105
111
|
|
|
112
|
+
this._heldCodes.delete(keyCode);
|
|
113
|
+
|
|
106
114
|
// если клавиша была зажата
|
|
107
115
|
if (this._pressedKeys[keyCode]) {
|
|
108
116
|
const name = this._currentKeySet[keyCode];
|
|
@@ -226,6 +234,25 @@ export default class ControlsModel {
|
|
|
226
234
|
this._releasePointer(this._lastTapX, this._lastTapY);
|
|
227
235
|
}
|
|
228
236
|
|
|
237
|
+
// снова отправляет нажатия удерживаемых команд (host-migration этап 4):
|
|
238
|
+
// при обрыве транспорта хост отпустил все клавиши игрока, а тот их так и
|
|
239
|
+
// держит. Зовётся после пакета входа — смена набора в нём уже сбросила
|
|
240
|
+
// _pressedKeys, поэтому источник — физически зажатые клавиши
|
|
241
|
+
resendHeld() {
|
|
242
|
+
if (this._areKeysEnabled === false || this._currentModes.chat) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
for (const keyCode of this._heldCodes) {
|
|
247
|
+
const name = this._currentKeySet[keyCode];
|
|
248
|
+
|
|
249
|
+
if (name && !this._pressedKeys[keyCode]) {
|
|
250
|
+
this._pressedKeys[keyCode] = true;
|
|
251
|
+
this.publisher.emit('socket', `down:${name}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
229
256
|
// задаёт возможность нажатия клавиш
|
|
230
257
|
// (stat и chat доступны при любом значении)
|
|
231
258
|
setKeysEnabled(isEnabled) {
|
|
@@ -214,7 +214,11 @@ export default class GamesModel {
|
|
|
214
214
|
return;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
this.publisher.emit('staged', {
|
|
217
|
+
this.publisher.emit('staged', {
|
|
218
|
+
id,
|
|
219
|
+
version: json.version,
|
|
220
|
+
manifest: json.manifest,
|
|
221
|
+
});
|
|
218
222
|
await this.loadAdmin();
|
|
219
223
|
}
|
|
220
224
|
|
|
@@ -18,16 +18,16 @@ export default class LobbyModel {
|
|
|
18
18
|
this._pageSize = params.pageSize;
|
|
19
19
|
this._pingInterval = params.pingInterval;
|
|
20
20
|
|
|
21
|
-
this._servers = new Map(); //
|
|
22
|
-
this._order = []; //
|
|
21
|
+
this._servers = new Map(); // roomId -> { ...server, latency }
|
|
22
|
+
this._order = []; // roomId в порядке выдачи мастером
|
|
23
23
|
this._total = 0; // всего серверов у мастера (для «Загрузить ещё»)
|
|
24
24
|
this._offset = 0;
|
|
25
25
|
this._search = '';
|
|
26
26
|
|
|
27
27
|
this._pingCounter = 0;
|
|
28
|
-
this._pending = new Map(); // pingId -> {
|
|
29
|
-
this._lastPing = new Map(); //
|
|
30
|
-
this._latencies = new Map(); //
|
|
28
|
+
this._pending = new Map(); // pingId -> { roomId, time }
|
|
29
|
+
this._lastPing = new Map(); // roomId -> time последнего пинга
|
|
30
|
+
this._latencies = new Map(); // roomId -> latency (переживает refresh)
|
|
31
31
|
|
|
32
32
|
// Leaderboard выбранной игры (lobby-page-plan) — сетевого I/O не делает,
|
|
33
33
|
// как и остальная модель; fetch живёт в main.js (fetchLeaderboard/fetchPlacement)
|
|
@@ -49,7 +49,7 @@ export default class LobbyModel {
|
|
|
49
49
|
this._emitFetch();
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
// задаёт поиск
|
|
52
|
+
// задаёт поиск (roomId, игра, карта) и запрашивает список заново
|
|
53
53
|
setSearch(text) {
|
|
54
54
|
this._search = typeof text === 'string' ? text.trim() : '';
|
|
55
55
|
this._offset = 0;
|
|
@@ -72,14 +72,18 @@ export default class LobbyModel {
|
|
|
72
72
|
this._total = total || 0;
|
|
73
73
|
|
|
74
74
|
(servers || []).forEach(server => {
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
// hostId — имя roomId у мастера до host-migration этапа 2
|
|
76
|
+
const roomId = server.roomId ?? server.hostId;
|
|
77
|
+
|
|
78
|
+
if (!this._servers.has(roomId)) {
|
|
79
|
+
this._order.push(roomId);
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
// latency живёт в _latencies и переживает refresh/пагинацию
|
|
80
|
-
this._servers.set(
|
|
83
|
+
this._servers.set(roomId, {
|
|
81
84
|
...server,
|
|
82
|
-
|
|
85
|
+
roomId,
|
|
86
|
+
latency: this._latencies.get(roomId) ?? null,
|
|
83
87
|
});
|
|
84
88
|
});
|
|
85
89
|
|
|
@@ -87,35 +91,35 @@ export default class LobbyModel {
|
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
// пользователь выбрал сервер — во внешнего подписчика (P2P-подключение).
|
|
90
|
-
// gameId едет вместе с
|
|
94
|
+
// gameId едет вместе с roomId: комната может быть по другой игре, и клиент
|
|
91
95
|
// обязан активировать её ClientPlugin до подключения
|
|
92
|
-
join(
|
|
93
|
-
const server = this._servers.get(
|
|
96
|
+
join(roomId) {
|
|
97
|
+
const server = this._servers.get(roomId);
|
|
94
98
|
|
|
95
99
|
if (server) {
|
|
96
|
-
this.publisher.emit('join', {
|
|
100
|
+
this.publisher.emit('join', { roomId, gameId: server.gameId });
|
|
97
101
|
}
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
// готовит пинг видимого сервера; false — если пинговали недавно/сервер ушёл
|
|
101
|
-
pingHost(
|
|
102
|
-
const last = this._lastPing.get(
|
|
105
|
+
pingHost(roomId, now) {
|
|
106
|
+
const last = this._lastPing.get(roomId);
|
|
103
107
|
|
|
104
108
|
if (last !== undefined && now - last < this._pingInterval) {
|
|
105
109
|
return false;
|
|
106
110
|
}
|
|
107
111
|
|
|
108
|
-
if (!this._servers.has(
|
|
112
|
+
if (!this._servers.has(roomId)) {
|
|
109
113
|
return false;
|
|
110
114
|
}
|
|
111
115
|
|
|
112
|
-
this._lastPing.set(
|
|
116
|
+
this._lastPing.set(roomId, now);
|
|
113
117
|
this._pingCounter = (this._pingCounter + 1) >>> 0;
|
|
114
118
|
|
|
115
119
|
const pingId = this._pingCounter;
|
|
116
120
|
|
|
117
|
-
this._pending.set(pingId, {
|
|
118
|
-
this.publisher.emit('ping-request', {
|
|
121
|
+
this._pending.set(pingId, { roomId, time: now });
|
|
122
|
+
this.publisher.emit('ping-request', { roomId, pingId });
|
|
119
123
|
|
|
120
124
|
return true;
|
|
121
125
|
}
|
|
@@ -130,17 +134,17 @@ export default class LobbyModel {
|
|
|
130
134
|
|
|
131
135
|
this._pending.delete(pingId);
|
|
132
136
|
|
|
133
|
-
const server = this._servers.get(pending.
|
|
137
|
+
const server = this._servers.get(pending.roomId);
|
|
134
138
|
|
|
135
139
|
if (!server) {
|
|
136
140
|
return;
|
|
137
141
|
}
|
|
138
142
|
|
|
139
143
|
server.latency = Math.round(now - pending.time);
|
|
140
|
-
this._latencies.set(pending.
|
|
144
|
+
this._latencies.set(pending.roomId, server.latency);
|
|
141
145
|
|
|
142
146
|
this.publisher.emit('ping-update', {
|
|
143
|
-
|
|
147
|
+
roomId: pending.roomId,
|
|
144
148
|
latency: server.latency,
|
|
145
149
|
});
|
|
146
150
|
}
|
|
@@ -157,7 +161,11 @@ export default class LobbyModel {
|
|
|
157
161
|
// позиция вызывающего, применяется ответом GET /auth/placement;
|
|
158
162
|
// placement === null — игрок ещё не ранжирован в этой игре
|
|
159
163
|
setPlacement({ placement, total, rank } = {}) {
|
|
160
|
-
this._myPlacement = {
|
|
164
|
+
this._myPlacement = {
|
|
165
|
+
placement: placement ?? null,
|
|
166
|
+
total: total || 0,
|
|
167
|
+
rank: rank || 0,
|
|
168
|
+
};
|
|
161
169
|
|
|
162
170
|
this._scheduleLeaderboardEmit();
|
|
163
171
|
}
|
|
@@ -60,9 +60,12 @@ export default class LobbyAuthModel {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// URL редиректа на старт OAuth-потока auth-сервиса; вызывающий должен сам
|
|
63
|
-
// сделать переход (window.location.href) — модель не трогает location
|
|
63
|
+
// сделать переход (window.location.href) — модель не трогает location.
|
|
64
|
+
// hash (#/<gameId>[/<roomId>]) едет в returnUrl: после логина игрок
|
|
65
|
+
// попадает туда, куда вёл открытый URL, а не в общее лобби
|
|
64
66
|
loginUrl(provider) {
|
|
65
|
-
const
|
|
67
|
+
const { origin, pathname, hash } = window.location;
|
|
68
|
+
const returnUrl = `${origin}${pathname}${hash}`;
|
|
66
69
|
|
|
67
70
|
return `${this._config.serviceUrl}/oauth/${provider}/start?returnUrl=${encodeURIComponent(returnUrl)}`;
|
|
68
71
|
}
|
|
@@ -125,6 +128,15 @@ export default class LobbyAuthModel {
|
|
|
125
128
|
return decodeJwtPayload(this._identityToken)?.role ?? 'user';
|
|
126
129
|
}
|
|
127
130
|
|
|
131
|
+
// срок входа (мс эпохи) или null — неизвестен. Вкладка с кончающимся
|
|
132
|
+
// токеном не принимает роль хоста: register_host/reclaim_host предъявят
|
|
133
|
+
// его мастеру посреди матча (host-migration-review этап 7)
|
|
134
|
+
getTokenExpiresAt() {
|
|
135
|
+
const exp = decodeJwtPayload(this._identityToken)?.exp;
|
|
136
|
+
|
|
137
|
+
return typeof exp === 'number' ? exp * 1000 : null;
|
|
138
|
+
}
|
|
139
|
+
|
|
128
140
|
_restore(tokenStorageKey) {
|
|
129
141
|
const token = localStorage[tokenStorageKey];
|
|
130
142
|
|
|
@@ -141,7 +153,8 @@ export default class LobbyAuthModel {
|
|
|
141
153
|
// показывает «залогинен» токеном, который хост уже отклонит при входе в игру
|
|
142
154
|
_setIdentity(token, tokenStorageKey, persist = true) {
|
|
143
155
|
const payload = decodeJwtPayload(token);
|
|
144
|
-
const isExpired =
|
|
156
|
+
const isExpired =
|
|
157
|
+
typeof payload?.exp === 'number' && Date.now() >= payload.exp * 1000;
|
|
145
158
|
|
|
146
159
|
if (!payload || !payload.nick || isExpired) {
|
|
147
160
|
delete localStorage[tokenStorageKey];
|
|
@@ -150,7 +163,10 @@ export default class LobbyAuthModel {
|
|
|
150
163
|
// битый токен — штатная ситуация возврата на сайт, не ошибка. login-error
|
|
151
164
|
// — только на интерактивном пути.
|
|
152
165
|
if (persist) {
|
|
153
|
-
this.publisher.emit(
|
|
166
|
+
this.publisher.emit(
|
|
167
|
+
'login-error',
|
|
168
|
+
isExpired ? 'tokenExpired' : 'invalidToken',
|
|
169
|
+
);
|
|
154
170
|
}
|
|
155
171
|
|
|
156
172
|
this.publisher.emit('login-required', this._config.providers);
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import Publisher from '../../../lib/Publisher.js';
|
|
2
|
+
|
|
3
|
+
// Singleton RoomMenuModel
|
|
4
|
+
//
|
|
5
|
+
// Меню комнаты внутри матча (host-migration, этапы 3 и 8d). Ссылка на
|
|
6
|
+
// текущую комнату — признак «вкладка в комнате». Пункты:
|
|
7
|
+
// - «Leave server» — у всех;
|
|
8
|
+
// - «Hand over host» — только у текущего хоста, когда в комнате есть другие
|
|
9
|
+
// люди и мастер назначил бету (иначе передавать некому).
|
|
10
|
+
// Пока идёт передача, пункты заблокированы, на их месте статус. Только
|
|
11
|
+
// лобби-режим: в solo/dedicated ссылки на комнату нет
|
|
12
|
+
|
|
13
|
+
// handoff: null | 'pending' | 'slow' | 'failed'
|
|
14
|
+
const STATUS_TEXT = {
|
|
15
|
+
pending: 'Handing over…',
|
|
16
|
+
slow: 'Slow connection…',
|
|
17
|
+
failed: 'Host handover failed',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
let roomMenuModel;
|
|
21
|
+
|
|
22
|
+
export default class RoomMenuModel {
|
|
23
|
+
constructor() {
|
|
24
|
+
if (roomMenuModel) {
|
|
25
|
+
return roomMenuModel;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
roomMenuModel = this;
|
|
29
|
+
|
|
30
|
+
this._link = null;
|
|
31
|
+
this._open = false;
|
|
32
|
+
this._role = null; // null | 'host' | 'guest'
|
|
33
|
+
this._othersPresent = false;
|
|
34
|
+
this._hasSuccessor = false;
|
|
35
|
+
this._handoff = null;
|
|
36
|
+
this.publisher = new Publisher();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// абсолютная ссылка на комнату; null — вкладка не в комнате, меню скрыто
|
|
40
|
+
setLink(link) {
|
|
41
|
+
this._link = link || null;
|
|
42
|
+
|
|
43
|
+
if (!this._link) {
|
|
44
|
+
this.setOpen(false);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this._emitState();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getLink() {
|
|
51
|
+
return this._link;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Роль вкладки в комнате.
|
|
56
|
+
* @param {Object} state
|
|
57
|
+
* @param {'host'|'guest'|null} state.role
|
|
58
|
+
* @param {boolean} [state.othersPresent] - в комнате есть другие люди.
|
|
59
|
+
* @param {boolean} [state.hasSuccessor] - мастер назначил бету.
|
|
60
|
+
*/
|
|
61
|
+
setRole({ role, othersPresent = false, hasSuccessor = false }) {
|
|
62
|
+
this._role = role ?? null;
|
|
63
|
+
this._othersPresent = othersPresent === true;
|
|
64
|
+
this._hasSuccessor = hasSuccessor === true;
|
|
65
|
+
this._emitState();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ход плановой передачи: null | 'pending' | 'slow' | 'failed'
|
|
69
|
+
setHandoff(status) {
|
|
70
|
+
this._handoff = status ?? null;
|
|
71
|
+
this._emitState();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
setOpen(open) {
|
|
75
|
+
const next = Boolean(open) && this._link !== null;
|
|
76
|
+
|
|
77
|
+
if (next === this._open) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this._open = next;
|
|
82
|
+
this.publisher.emit('open', next);
|
|
83
|
+
|
|
84
|
+
// ошибка передачи показана — закрытое меню её забывает
|
|
85
|
+
if (!next && this._handoff === 'failed') {
|
|
86
|
+
this.setHandoff(null);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
toggle() {
|
|
91
|
+
this.setOpen(!this._open);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Что показывать.
|
|
96
|
+
* @returns {{ visible: boolean, leave: Object, handover: Object,
|
|
97
|
+
* status: string|null }} leave/handover — { visible, disabled }.
|
|
98
|
+
*/
|
|
99
|
+
getState() {
|
|
100
|
+
const inRoom = this._link !== null;
|
|
101
|
+
const busy = this._handoff === 'pending' || this._handoff === 'slow';
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
visible: inRoom,
|
|
105
|
+
leave: { visible: inRoom, disabled: busy },
|
|
106
|
+
handover: {
|
|
107
|
+
visible:
|
|
108
|
+
inRoom &&
|
|
109
|
+
this._role === 'host' &&
|
|
110
|
+
this._othersPresent &&
|
|
111
|
+
this._hasSuccessor,
|
|
112
|
+
disabled: busy,
|
|
113
|
+
},
|
|
114
|
+
status: STATUS_TEXT[this._handoff] ?? null,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_emitState() {
|
|
119
|
+
this.publisher.emit('state', this.getState());
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -174,7 +174,8 @@ export default class StatModel {
|
|
|
174
174
|
|
|
175
175
|
// в топе игрока нет — его собственное место знает только рассылка хоста
|
|
176
176
|
const myId = this._localPlayer?.id ?? null;
|
|
177
|
-
const mine =
|
|
177
|
+
const mine =
|
|
178
|
+
myId === null ? null : this._accolades.selfOf(myId, this._period);
|
|
178
179
|
|
|
179
180
|
if (!mine || !nick) {
|
|
180
181
|
return marked;
|