vimp-engine 0.35.6 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/bin/vimp-sim.js +19 -1
  2. package/core/Cargo.toml +1 -1
  3. package/package.json +1 -1
  4. package/src/client/components/controller/Controls.js +5 -0
  5. package/src/client/components/controller/Lobby.js +4 -4
  6. package/src/client/components/controller/RoomMenu.js +43 -0
  7. package/src/client/components/controller/Vote.js +18 -2
  8. package/src/client/components/model/Chat.js +19 -2
  9. package/src/client/components/model/ClientReports.js +6 -2
  10. package/src/client/components/model/Controls.js +27 -0
  11. package/src/client/components/model/Games.js +5 -1
  12. package/src/client/components/model/Lobby.js +32 -24
  13. package/src/client/components/model/LobbyAuth.js +20 -4
  14. package/src/client/components/model/RoomMenu.js +121 -0
  15. package/src/client/components/model/Stat.js +2 -1
  16. package/src/client/components/model/Vote.js +122 -4
  17. package/src/client/components/view/Auth.js +20 -10
  18. package/src/client/components/view/ClientReports.js +16 -5
  19. package/src/client/components/view/Games.js +39 -14
  20. package/src/client/components/view/Lobby.js +90 -43
  21. package/src/client/components/view/LobbyAuth.js +13 -7
  22. package/src/client/components/view/Panel.js +1 -2
  23. package/src/client/components/view/RoomMenu.js +69 -0
  24. package/src/client/debug.js +24 -1
  25. package/src/client/lib/FpsMeter.js +46 -0
  26. package/src/client/lib/JoinRetry.js +73 -0
  27. package/src/client/lib/diagnostics.js +28 -5
  28. package/src/client/lib/hostCaps.js +82 -0
  29. package/src/client/lib/hostUnloadGuard.js +108 -0
  30. package/src/client/lib/hostVoteCommand.js +92 -0
  31. package/src/client/lib/pageReload.js +29 -0
  32. package/src/client/lib/roomLink.js +257 -0
  33. package/src/client/lib/signalingErrors.js +74 -0
  34. package/src/client/main.js +676 -451
  35. package/src/client/network/HiddenHostHealthLog.js +76 -0
  36. package/src/client/network/HostConnectionManager.js +62 -8
  37. package/src/client/network/HostController.js +376 -23
  38. package/src/client/network/HostHealthPolicy.js +307 -0
  39. package/src/client/network/HostHealthReporter.js +54 -0
  40. package/src/client/network/HostPrewarm.js +235 -0
  41. package/src/client/network/LoopbackTransport.js +16 -1
  42. package/src/client/network/PlannedHandoff.js +366 -0
  43. package/src/client/network/Promotion.js +420 -0
  44. package/src/client/network/RoomPeersReporter.js +81 -0
  45. package/src/client/network/SessionSupervisor.js +730 -0
  46. package/src/client/network/SignalingClient.js +183 -30
  47. package/src/client/network/StandbyReceiver.js +287 -0
  48. package/src/client/network/StandbySender.js +319 -0
  49. package/src/client/network/TokenHandoffTimer.js +81 -0
  50. package/src/client/network/WebRtcManager.js +338 -11
  51. package/src/client/network/policyClose.js +32 -2
  52. package/src/client/network/standbyChunks.js +185 -0
  53. package/src/client/session/GuestSession.js +428 -0
  54. package/src/client/session/HandoffFlow.js +399 -0
  55. package/src/client/session/HostRole.js +708 -0
  56. package/src/client/session/Membership.js +179 -0
  57. package/src/client/session/PromotionFlow.js +325 -0
  58. package/src/client/session/RouteBoot.js +432 -0
  59. package/src/client/session/StandbyRole.js +195 -0
  60. package/src/client/session/hostRoomPrep.js +152 -0
  61. package/src/client/session/roomContext.js +32 -0
  62. package/src/client/style.css +83 -22
  63. package/src/client/views/gameShell.js +7 -0
  64. package/src/client/views/includes/informer.pug +4 -0
  65. package/src/client/views/includes/lobby.pug +0 -3
  66. package/src/client/views/includes/roomMenu.pug +9 -0
  67. package/src/client/views/index.pug +1 -0
  68. package/src/config/chatMessages.js +45 -0
  69. package/src/config/closeCodes.js +1 -1
  70. package/src/config/env.js +37 -0
  71. package/src/config/hostDefaults.js +24 -0
  72. package/src/config/lobby.js +154 -3
  73. package/src/config/master.js +104 -21
  74. package/src/config/wsports.js +8 -0
  75. package/src/devtools/RecordingSocketManager.js +3 -2
  76. package/src/devtools/ScenarioRunner.js +123 -8
  77. package/src/devtools/VirtualClient.js +20 -0
  78. package/src/devtools/contract/format.js +4 -1
  79. package/src/devtools/contract/index.js +1 -2
  80. package/src/devtools/contract/rules/a2-package-scripts.js +1 -3
  81. package/src/devtools/contract/rules/b8-system-messages.js +2 -2
  82. package/src/devtools/contract/rules/b9-vote-names.js +25 -1
  83. package/src/devtools/contract/rules/c11-stat-mode.js +4 -1
  84. package/src/devtools/contract/rules/c6-stat-columns.js +1 -1
  85. package/src/devtools/contract/rules/c7-key-sets.js +8 -4
  86. package/src/devtools/contract/rules/d3-snapshot-interp.js +2 -1
  87. package/src/devtools/contract/rules/e1-sound-pairs.js +3 -1
  88. package/src/devtools/contract/rules/e4-map-layers.js +13 -2
  89. package/src/devtools/contract/rules/e5-map-radar-walls.js +7 -1
  90. package/src/devtools/contract/rules/index.js +38 -5
  91. package/src/devtools/invariants.js +10 -4
  92. package/src/devtools/pluginLoader.js +1 -4
  93. package/src/devtools/report.js +17 -1
  94. package/src/host/DebugRecorder.js +6 -1
  95. package/src/host/GameCoreAdapter.js +45 -0
  96. package/src/host/HostGame.js +928 -59
  97. package/src/host/PortMachine.js +268 -20
  98. package/src/host/host.worker.js +222 -16
  99. package/src/host/meta/SocketManager.js +20 -0
  100. package/src/host/meta/core/RoundManager.js +89 -14
  101. package/src/host/meta/modules/Accolades.js +4 -1
  102. package/src/host/meta/modules/Panel.js +30 -0
  103. package/src/host/meta/modules/PlayerDataSync.js +255 -46
  104. package/src/host/meta/modules/RTTManager.js +34 -0
  105. package/src/host/meta/modules/TimerManager.js +215 -18
  106. package/src/host/meta/modules/chat/systemMessages.js +29 -3
  107. package/src/host/meta/player/HumanParticipant.js +10 -0
  108. package/src/host/meta/player/ParticipantManager.js +98 -5
  109. package/src/lib/AbstractTimer.js +63 -1
  110. package/src/lib/Publisher.js +26 -0
  111. package/src/lib/capabilities.js +5 -0
  112. package/src/lib/checkpointCodec.js +125 -0
  113. package/src/lib/clientIp.js +9 -2
  114. package/src/lib/createHostRuntime.js +95 -6
  115. package/src/lib/formOptions.js +3 -1
  116. package/src/lib/gameConfigView.js +10 -0
  117. package/src/lib/gameId.js +13 -0
  118. package/src/lib/jwt.js +3 -1
  119. package/src/lib/loadGamePackage.js +2 -2
  120. package/src/lib/resumeKey.js +38 -0
  121. package/src/lib/roomId.js +39 -0
  122. package/src/lib/roomSettings.js +59 -0
  123. package/src/lib/validators.js +7 -0
  124. package/tests/fixtures/generations/gen-api3/host/ScriptedManager.js +2 -3
  125. package/tests/fixtures/generations/gen-api3/host/fakeCore.js +31 -5
  126. package/tests/fixtures/generations/gen-api3/manifest.json +1 -3
  127. package/tests/fixtures/generations/gen-api4/host/ScriptedManager.js +2 -3
  128. package/tests/fixtures/generations/gen-api4/host/fakeCore.js +31 -5
  129. package/tests/fixtures/generations/gen-api4/manifest.json +1 -3
  130. package/tests/fixtures/miniGame/config/client.js +0 -20
  131. package/tests/fixtures/miniGame/host/ScriptedManager.js +2 -3
  132. package/tests/fixtures/miniGame/host/fakeCore.js +31 -5
@@ -0,0 +1,257 @@
1
+ // Роутер прямых ссылок (host-migration, этап 3). Три вида URL лобби-режима:
2
+ //
3
+ // без hash — лобби (все игры)
4
+ // #/<gameId> — быстрая игра: оптимальная комната игры
5
+ // #/<gameId>/<roomId> — прямой вход в комнату
6
+ //
7
+ // Маршрут живёт в hash: страница одна (SPA), сервер про маршруты не знает, а
8
+ // OAuth-возврат hash сохраняет (returnUrl LobbyAuthModel.loginUrl). Решения
9
+ // «что делать с маршрутом» и «куда уходить из комнаты» — чистые функции:
10
+ // main.js исполняется при импорте, и проверить ветвление внутри него нечем.
11
+
12
+ import { isValidGameId } from '../../lib/gameId.js';
13
+ import { isValidRoomId } from '../../lib/roomId.js';
14
+ import { isGameAvailable } from './pickActiveGame.js';
15
+
16
+ const NONE = Object.freeze({ kind: 'none' });
17
+
18
+ /**
19
+ * Разбирает hash страницы. Чужие hash (`#auth`, пустой, мусор) — `none`:
20
+ * роутер их не трогает.
21
+ * @param {string} hash - location.hash (с `#` или без).
22
+ * @returns {{kind: 'room', gameId: string, roomId: string}
23
+ * | {kind: 'game', gameId: string} | {kind: 'none'}}
24
+ */
25
+ export function parseRoute(hash) {
26
+ if (typeof hash !== 'string') {
27
+ return NONE;
28
+ }
29
+
30
+ const body = hash.startsWith('#') ? hash.slice(1) : hash;
31
+
32
+ if (!body.startsWith('/')) {
33
+ return NONE;
34
+ }
35
+
36
+ // ссылку набирают и руками: регистр и хвостовой слэш не повод для отказа
37
+ const parts = body.slice(1).toLowerCase().split('/');
38
+
39
+ if (parts.length > 1 && parts[parts.length - 1] === '') {
40
+ parts.pop();
41
+ }
42
+
43
+ const [gameId, roomId] = parts;
44
+
45
+ if (!isValidGameId(gameId) || parts.length > 2) {
46
+ return NONE;
47
+ }
48
+
49
+ if (parts.length === 1) {
50
+ return { kind: 'game', gameId };
51
+ }
52
+
53
+ return isValidRoomId(roomId) ? { kind: 'room', gameId, roomId } : NONE;
54
+ }
55
+
56
+ /**
57
+ * @param {string} gameId
58
+ * @param {string} roomId
59
+ * @returns {string} hash-часть ссылки на комнату.
60
+ */
61
+ export function formatRoomLink(gameId, roomId) {
62
+ return `#/${gameId}/${roomId}`;
63
+ }
64
+
65
+ /**
66
+ * @param {string} gameId
67
+ * @returns {string} hash-часть ссылки быстрой игры.
68
+ */
69
+ export function formatGameLink(gameId) {
70
+ return `#/${gameId}`;
71
+ }
72
+
73
+ /**
74
+ * @param {string} hashPart - `#/…` или '' (лобби).
75
+ * @param {Location} [loc]
76
+ * @returns {string} Абсолютная ссылка для буфера обмена.
77
+ */
78
+ export function absoluteLink(hashPart, loc = window.location) {
79
+ return `${loc.origin}${loc.pathname}${hashPart}`;
80
+ }
81
+
82
+ /**
83
+ * Меняет адрес без перезагрузки и без записи в историю: «назад» не должен
84
+ * возвращать в комнату, из которой ушли. Query отбрасывается (там бывают
85
+ * только токены OAuth-возврата, они уже прочитаны).
86
+ * @param {string} hashPart - `#/…` или '' (лобби).
87
+ */
88
+ export function setRoute(hashPart) {
89
+ window.history.replaceState(
90
+ null,
91
+ '',
92
+ `${window.location.pathname}${hashPart}`,
93
+ );
94
+ }
95
+
96
+ /**
97
+ * Перезагрузка на маршрут. replace() с одним только другим hash страницу не
98
+ * перезагружает — поэтому явный reload() следом.
99
+ * @param {string} hashPart - `#/…` или '' (лобби).
100
+ */
101
+ export function reloadTo(hashPart) {
102
+ setRoute(hashPart);
103
+ // единственный потребитель — pageReload.js (сначала guard.exit())
104
+ // eslint-disable-next-line no-restricted-syntax
105
+ window.location.reload();
106
+ }
107
+
108
+ /**
109
+ * Играбельна ли игра в каталоге вкладки.
110
+ * @param {Map<string, Object>} catalog - gamesById.
111
+ * @param {string} gameId
112
+ * @returns {boolean}
113
+ */
114
+ function isPlayable(catalog, gameId) {
115
+ const manifest = catalog.get(gameId);
116
+
117
+ return Boolean(manifest) && isGameAvailable(manifest);
118
+ }
119
+
120
+ /**
121
+ * Что делать с маршрутом при загрузке страницы.
122
+ * @param {Object} route - Результат parseRoute.
123
+ * @param {Object|null} roomInfo - Ответ GET /rooms/:roomId (null — комнаты
124
+ * нет или запрос не удался); для маршрута `game` не используется.
125
+ * @param {Map<string, Object>} catalog - Каталог манифестов вкладки.
126
+ * @returns {{action: 'lobby', informer?: string}
127
+ * | {action: 'join', gameId: string, roomId: string}
128
+ * | {action: 'wait', gameId: string, roomId: string}
129
+ * | {action: 'quickPlay', gameId: string}}
130
+ */
131
+ export function decideRouteAction(route, roomInfo, catalog) {
132
+ if (route.kind === 'none') {
133
+ return { action: 'lobby' };
134
+ }
135
+
136
+ // комната другой игры, чем в ссылке: верим комнате (ссылку могли
137
+ // отредактировать руками)
138
+ const gameId =
139
+ route.kind === 'room' && roomInfo?.gameId ? roomInfo.gameId : route.gameId;
140
+
141
+ if (!isPlayable(catalog, gameId)) {
142
+ return {
143
+ action: 'lobby',
144
+ informer: `Game "${gameId}" is not available.`,
145
+ };
146
+ }
147
+
148
+ // комната меняет хоста (host-migration этап 7): она жива, просто войти
149
+ // в неё сейчас не к кому — ждать, пока снова станет online
150
+ if (route.kind === 'room' && roomInfo && isMigrating(roomInfo)) {
151
+ return { action: 'wait', gameId, roomId: route.roomId };
152
+ }
153
+
154
+ // мёртвая или полная комната — быстрая игра той же игры (полные она
155
+ // отбрасывает сама)
156
+ if (
157
+ route.kind === 'game' ||
158
+ !roomInfo ||
159
+ roomInfo.currentPlayers >= roomInfo.maxPlayers
160
+ ) {
161
+ return { action: 'quickPlay', gameId };
162
+ }
163
+
164
+ return { action: 'join', gameId, roomId: route.roomId };
165
+ }
166
+
167
+ /**
168
+ * Комната по ссылке в смене хоста (поле status ответа GET /rooms/:roomId;
169
+ * мастер до этапа 7 его не отдавал — такая комната считается online).
170
+ * handing_off — плановая передача (этап 8): офферы мастер отклоняет так же.
171
+ * @param {Object} roomInfo
172
+ * @returns {boolean}
173
+ */
174
+ export function isMigrating(roomInfo) {
175
+ return roomInfo?.status === 'migrating' || roomInfo?.status === 'handing_off';
176
+ }
177
+
178
+ /**
179
+ * Итог одного опроса комнаты, которая меняет хоста.
180
+ * @param {Object} poll
181
+ * @param {number|null} poll.status - HTTP-статус GET /rooms/:roomId (null —
182
+ * запрос не дошёл).
183
+ * @param {Object|null} [poll.info] - Тело ответа.
184
+ * @returns {'online'|'gone'|'retry'} gone — только 404: сбой сети или 5xx
185
+ * посреди миграции не повод уводить игрока из живой комнаты.
186
+ */
187
+ export function classifyRoomPoll({ status, info = null }) {
188
+ if (status === 404) {
189
+ return 'gone';
190
+ }
191
+
192
+ if (status >= 200 && status < 300 && info && !isMigrating(info)) {
193
+ return 'online';
194
+ }
195
+
196
+ return 'retry';
197
+ }
198
+
199
+ /**
200
+ * Пауза быстрой игры перед созданием своей комнаты: гости закрытой комнаты
201
+ * приходят разом, случайный разброс даёт первому создать комнату, а
202
+ * остальным — найти её повторным GET /servers.
203
+ * @param {Object} range - { createDelayMinMs, createDelayMaxMs }.
204
+ * @param {Function} [random] - Math.random (тесты).
205
+ * @returns {number} мс.
206
+ */
207
+ export function quickPlayCreateDelay(
208
+ { createDelayMinMs = 0, createDelayMaxMs = 0 } = {},
209
+ random = Math.random,
210
+ ) {
211
+ const min = Math.max(0, createDelayMinMs);
212
+ const max = Math.max(min, createDelayMaxMs);
213
+
214
+ return Math.round(min + (max - min) * random());
215
+ }
216
+
217
+ /**
218
+ * Оптимальная комната быстрой игры: строго этой игры, не полная, с
219
+ * максимумом игроков (при равенстве — первая в списке мастера).
220
+ * @param {Array<Object>} servers - servers из GET /servers.
221
+ * @param {string} gameId
222
+ * @returns {Object|null}
223
+ */
224
+ export function pickQuickPlayRoom(servers, gameId) {
225
+ let best = null;
226
+
227
+ for (const server of servers ?? []) {
228
+ if (
229
+ server.gameId === gameId &&
230
+ server.currentPlayers < server.maxPlayers &&
231
+ (!best || server.currentPlayers > best.currentPlayers)
232
+ ) {
233
+ best = server;
234
+ }
235
+ }
236
+
237
+ return best;
238
+ }
239
+
240
+ /**
241
+ * Куда уходить после разрыва с комнатой (лобби-режим).
242
+ * @param {Object} reason
243
+ * @param {boolean} reason.kicked - Кик хоста (policyClose.isKickClose).
244
+ * @param {string} [reason.gameId] - Игра комнаты.
245
+ * @returns {{route: string} | {reload: string}} route — сменить адрес без
246
+ * перезагрузки (причина остаётся в #tech-informer); reload — перезагрузка
247
+ * на маршрут.
248
+ */
249
+ export function decideExitRoute({ kicked, gameId }) {
250
+ // кикнутого — на главную: быстрая игра вернула бы его в ту же комнату
251
+ if (kicked) {
252
+ return { route: '' };
253
+ }
254
+
255
+ // комната закрылась — в быструю игру той же игры
256
+ return { reload: gameId ? formatGameLink(gameId) : '' };
257
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Что делать с error {code: 'unknownRoom'} мастера.
3
+ * @param {Object} ctx
4
+ * @param {Object} ctx.msg - сообщение ошибки ({ code, re, roomId }).
5
+ * @param {string|null} ctx.currentRoomId
6
+ * @param {boolean} ctx.promoting - вкладка занимает комнату (hostPromotion).
7
+ * @param {string|null} ctx.sessionState - состояние SessionSupervisor.
8
+ * @param {boolean} ctx.offerPending - текущий транспорт — WebRTC, ещё не открыт.
9
+ * @param {boolean} [ctx.memberJoined] - вкладка объявила членство join_room.
10
+ * @param {boolean} [ctx.isHost] - вкладка — хост комнаты.
11
+ * @returns {'abandonPromotion'|'leave'|'retryJoin'|'ignore'}
12
+ */
13
+ export function decideUnknownRoom({
14
+ msg,
15
+ currentRoomId,
16
+ promoting,
17
+ sessionState,
18
+ offerPending,
19
+ memberJoined = false,
20
+ isHost = false,
21
+ }) {
22
+ if (msg.roomId && currentRoomId && msg.roomId !== currentRoomId) {
23
+ return 'ignore';
24
+ }
25
+
26
+ if (promoting && (!msg.re || msg.re === 'register_host')) {
27
+ return 'abandonPromotion';
28
+ }
29
+
30
+ // мастер рестартовал, а хост ещё не вернул комнату reclaim_host — вход
31
+ // повторяется (P2P-матч при этом жив)
32
+ if (msg.re === 'join_room') {
33
+ return msg.roomId === currentRoomId &&
34
+ !isHost &&
35
+ memberJoined &&
36
+ sessionState !== 'closed'
37
+ ? 'retryJoin'
38
+ : 'ignore';
39
+ }
40
+
41
+ // комната сменяет хоста: исход решат host_changed / room_closed и таймер
42
+ // миграции супервизора, а не ответ на оффер, ушедший в момент смены
43
+ if (sessionState === 'migrating') {
44
+ return 'ignore';
45
+ }
46
+
47
+ if ((!msg.re || msg.re === 'webrtc_offer') && offerPending) {
48
+ return 'leave';
49
+ }
50
+
51
+ return 'ignore';
52
+ }
53
+
54
+ /**
55
+ * Что делать с error {code: 'invalidToken'} мастера (истёкший вход).
56
+ * @param {Object} ctx
57
+ * @param {Object} ctx.msg - сообщение ошибки ({ code, re, roomId }).
58
+ * @param {boolean} ctx.promoting - вкладка занимает комнату (hostPromotion).
59
+ * @param {boolean} ctx.inRoom - вкладка в комнате.
60
+ * @param {string|null} ctx.sessionState - состояние SessionSupervisor.
61
+ * @returns {'abandonPromotion'|'keepPlaying'|'logoutAndLeave'|'logout'}
62
+ */
63
+ export function decideInvalidToken({ msg, promoting, inRoom, sessionState }) {
64
+ if (msg.re === 'register_host' && promoting) {
65
+ return 'abandonPromotion';
66
+ }
67
+
68
+ // гость в живом матче: токен нужен мастеру, а не P2P-соединению с хостом
69
+ if (msg.re === 'join_room' && inRoom && sessionState !== 'closed') {
70
+ return 'keepPlaying';
71
+ }
72
+
73
+ return inRoom ? 'logoutAndLeave' : 'logout';
74
+ }