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,366 @@
1
+ // Плановая передача роли хоста (host-migration этап 8.2): вкладка-хост
2
+ // отдаёт комнату бете без отката. handoff_begin мастеру → handoff_go →
3
+ // заморозка Worker'а и финальная контрольная точка (её везёт StandbySender
4
+ // с приоритетом) → host_released: бета заняла комнату с того же тика.
5
+ // Дальше — владелец (client/session/HandoffFlow.js): stay — свой игрок
6
+ // возвращается гостем RESUME'ом к новому хосту, иначе — уход в лобби.
7
+ //
8
+ // Исход у передачи один, и объявляется он один раз. Дедлайн deadlineMs
9
+ // отсчитывается от handoff_begin и перекрывает handoffTimeoutMs мастера с
10
+ // запасом на дорогу ответа: обычно отмену объявляет мастер
11
+ // (handoff_aborted), дедлайн — страховка. Медленный ответ (нет handoff_go за
12
+ // slowAfterMs) — не отказ, а повод показать «медленная связь» (onSlow):
13
+ // поздний handoff_go до дедлайна штатно замораживает матч и шлёт финальную
14
+ // точку — иначе бета откатила бы всех к периодической.
15
+ //
16
+ // Отмена: handoff_unavailable (матч не замораживался), handoff_aborted,
17
+ // обрыв сигналинга или дедлайн — замороженный матч размораживается, вкладка
18
+ // остаётся хостом. Уходящий хост (stay: false) уходит всё равно:
19
+ // host_leaving мастеру — аварийная миграция с последней периодической
20
+ // точкой (при обрыве сигналинга без него: обрыв WS мастер видит сам).
21
+ //
22
+ // Отложенная передача (defer, этап 8d): handoff_begin уходит на границе
23
+ // раунда (HostController.awaitRoundBoundary — игра без migration.midRound
24
+ // иначе отняла бы раунд у всех; у игры с midRound Worker отвечает сразу),
25
+ // но не позже deferMaxMs. Пока ожидание идёт, передача уже начата: повтор
26
+ // игнорируется, а дедлайн и onSlow отсчитываются от handoff_begin.
27
+ // Автотриггеры (этап 9b) ожидание сокращают (hurry — жёсткая перегрузка,
28
+ // скрытая вкладка) или снимают (cancelDeferred — нагрузка нормализовалась).
29
+ //
30
+ // Без DOM: сигналинг, Worker хоста и таймеры инъектируются.
31
+
32
+ const SIGNALING_EVENTS = [
33
+ ['handoff_go', '_onGo'],
34
+ ['handoff_unavailable', '_onUnavailable'],
35
+ ['handoff_aborted', '_onAborted'],
36
+ ['host_released', '_onReleased'],
37
+ ['close', '_onSignalingClose'],
38
+ ];
39
+
40
+ /**
41
+ * Что вернуть своему игроку после отменённой передачи. Оверлей паузы
42
+ * передачи снимается всегда, а ввод и звук — только живому месту в матче:
43
+ * игрок, потерянный раньше (handleOwnPlayerLost), остаётся без них.
44
+ * @param {Object} params
45
+ * @param {boolean} params.frozen - матч замораживался (был оверлей).
46
+ * @param {string|null} params.sessionState - состояние SessionSupervisor.
47
+ * @returns {{ hideOverlay: boolean, restoreInput: boolean }}
48
+ */
49
+ export function controlsAfterAbort({ frozen, sessionState }) {
50
+ return {
51
+ hideOverlay: frozen,
52
+ restoreInput: frozen && sessionState === 'inGame',
53
+ };
54
+ }
55
+
56
+ export default class PlannedHandoff {
57
+ /**
58
+ * @param {Object} options
59
+ * @param {SignalingClient} options.signaling
60
+ * @param {Function} options.getRoom - () => { roomId, epoch } | null —
61
+ * комната, которую вкладка хостит сейчас.
62
+ * @param {Function} options.getController - () => HostController | null.
63
+ * @param {number} [options.slowAfterMs] - нет handoff_go за это время —
64
+ * onSlow (передача продолжается).
65
+ * @param {number} [options.deadlineMs] - общий дедлайн передачи от
66
+ * handoff_begin (больше handoffTimeoutMs мастера).
67
+ * @param {number} [options.deferMaxMs] - потолок ожидания границы раунда
68
+ * отложенной передачи.
69
+ * @param {Function} [options.onSlow] - () мастер отвечает медленно.
70
+ * @param {Function} [options.onFrozen] - ({ epoch }) матч заморожен,
71
+ * финальная точка запрошена; epoch — эпоха преемника.
72
+ * @param {Function} [options.onReleased] - ({ stay, epoch }) роль отдана.
73
+ * @param {Function} [options.onAborted] - ({ reason, frozen }) передача
74
+ * окончательно отменена, вкладка остаётся хостом (только stay).
75
+ * @param {Function} [options.onLeave] - ({ reason }) передать не вышло, а
76
+ * хост уходит (stay: false) — дальше аварийный путь мастера.
77
+ * @param {Object} [options.timers] - { setTimeout, clearTimeout } (тесты).
78
+ */
79
+ constructor({
80
+ signaling,
81
+ getRoom,
82
+ getController,
83
+ slowAfterMs = 3000,
84
+ deadlineMs = 10000,
85
+ deferMaxMs = 30000,
86
+ onSlow = () => {},
87
+ onFrozen = () => {},
88
+ onReleased = () => {},
89
+ onAborted = () => {},
90
+ onLeave = () => {},
91
+ timers = globalThis,
92
+ }) {
93
+ this._signaling = signaling;
94
+ this._getRoom = getRoom;
95
+ this._getController = getController;
96
+ this._slowAfterMs = slowAfterMs;
97
+ this._deadlineMs = deadlineMs;
98
+ this._deferMaxMs = deferMaxMs;
99
+ this._onSlow = onSlow;
100
+ this._onFrozen = onFrozen;
101
+ this._onReleasedCb = onReleased;
102
+ this._onAbortedCb = onAborted;
103
+ this._onLeave = onLeave;
104
+ this._timers = timers;
105
+
106
+ // null | 'deferred' (ждём границы раунда) | 'requested' (ждём
107
+ // handoff_go) | 'frozen' (ждём host_released)
108
+ this._phase = null;
109
+ this._room = null;
110
+ this._reason = null;
111
+ this._stay = true;
112
+ this._slowTimer = null;
113
+ this._deadlineTimer = null;
114
+ this._deferTimer = null;
115
+ }
116
+
117
+ // передача идёт: повторный start игнорируется
118
+ get active() {
119
+ return this._phase !== null;
120
+ }
121
+
122
+ // передача ждёт границы раунда: матч ещё идёт, мастер ещё не спрошен
123
+ get deferred() {
124
+ return this._phase === 'deferred';
125
+ }
126
+
127
+ // причина идущей передачи (null — передачи нет)
128
+ get reason() {
129
+ return this._reason;
130
+ }
131
+
132
+ // идёт передача уходящего хоста (stay: false)
133
+ get leaving() {
134
+ return this.active && !this._stay;
135
+ }
136
+
137
+ /**
138
+ * Начать передачу.
139
+ * @param {Object} options
140
+ * @param {string} options.reason - 'leave' | 'handover' | 'overload' |
141
+ * 'hidden' | 'network' (этап 9) | 'vote' (этап 10).
142
+ * @param {boolean} [options.stay] - остаться в комнате гостем.
143
+ * @param {boolean} [options.defer] - дождаться границы раунда.
144
+ * @returns {boolean} false — передача уже идёт или вкладка не хост.
145
+ */
146
+ start({ reason, stay = true, defer = false }) {
147
+ const room = this._getRoom();
148
+ const controller = this._getController();
149
+
150
+ if (this.active || !room || !controller) {
151
+ return false;
152
+ }
153
+
154
+ this._room = { roomId: room.roomId, epoch: room.epoch };
155
+ this._reason = reason;
156
+ this._stay = stay === true;
157
+
158
+ for (const [type, handler] of SIGNALING_EVENTS) {
159
+ this._signaling.publisher.on(type, handler, this);
160
+ }
161
+
162
+ if (!defer) {
163
+ this._begin();
164
+ return true;
165
+ }
166
+
167
+ this._phase = 'deferred';
168
+ this._deferTimer = this._timers.setTimeout(() => {
169
+ this._deferTimer = null;
170
+ this._getController()?.cancelRoundBoundary();
171
+ this._begin();
172
+ }, this._deferMaxMs);
173
+ controller.awaitRoundBoundary(() => this._onBoundary());
174
+
175
+ return true;
176
+ }
177
+
178
+ /**
179
+ * Не ждать границы раунда: вопрос мастеру сразу, с новой причиной.
180
+ * @param {string} [reason] - причина вместо исходной.
181
+ * @returns {boolean} false — передача не в ожидании границы.
182
+ */
183
+ hurry(reason = this._reason) {
184
+ if (this._phase !== 'deferred') {
185
+ return false;
186
+ }
187
+
188
+ this._clearDeferTimer();
189
+ this._getController()?.cancelRoundBoundary();
190
+ this._reason = reason;
191
+ this._begin();
192
+
193
+ return true;
194
+ }
195
+
196
+ /**
197
+ * Снять передачу, ждущую границы раунда, — тихо, без колбэков (мастер
198
+ * ещё не спрашивался, матч не замораживался).
199
+ * @returns {boolean} false — передача не в ожидании границы.
200
+ */
201
+ cancelDeferred() {
202
+ if (this._phase !== 'deferred') {
203
+ return false;
204
+ }
205
+
206
+ this._finish();
207
+
208
+ return true;
209
+ }
210
+
211
+ // граница раунда наступила (у игры с midRound — сразу)
212
+ _onBoundary() {
213
+ if (this._phase !== 'deferred') {
214
+ return;
215
+ }
216
+
217
+ this._clearDeferTimer();
218
+ this._begin();
219
+ }
220
+
221
+ // вопрос мастеру: с этого момента идут onSlow и дедлайн
222
+ _begin() {
223
+ const room = this._room;
224
+
225
+ this._phase = 'requested';
226
+
227
+ this._slowTimer = this._timers.setTimeout(() => {
228
+ this._slowTimer = null;
229
+ this._onSlow();
230
+ }, this._slowAfterMs);
231
+
232
+ this._deadlineTimer = this._timers.setTimeout(() => {
233
+ this._deadlineTimer = null;
234
+ this._cancel('timeout');
235
+ }, this._deadlineMs);
236
+
237
+ this._signaling.handoffBegin({
238
+ roomId: room.roomId,
239
+ epoch: room.epoch,
240
+ reason: this._reason,
241
+ stay: this._stay,
242
+ });
243
+ }
244
+
245
+ // владелец снимает роль хоста сам (teardown): тихо, без колбэков
246
+ abort() {
247
+ this._finish();
248
+ }
249
+
250
+ _ownRoom(msg) {
251
+ return this.active && msg?.roomId === this._room.roomId;
252
+ }
253
+
254
+ _onGo(msg) {
255
+ if (!this._ownRoom(msg) || this._phase !== 'requested') {
256
+ return;
257
+ }
258
+
259
+ this._clearSlowTimer();
260
+ this._phase = 'frozen';
261
+
262
+ // цикл встаёт на границе кадра, точка снимается сразу — с того же тика
263
+ const controller = this._getController();
264
+
265
+ controller?.freeze();
266
+ controller?.requestCheckpoint({ final: true });
267
+
268
+ this._onFrozen({ epoch: msg.epoch });
269
+ }
270
+
271
+ _onUnavailable(msg) {
272
+ if (this._ownRoom(msg) && this._phase === 'requested') {
273
+ this._cancel(msg.reason ?? 'unavailable');
274
+ }
275
+ }
276
+
277
+ // бета не заняла комнату: эпоха прежняя, матч продолжается здесь.
278
+ // Отложенная передача мастера ещё не спрашивала — отмена не про неё
279
+ _onAborted(msg) {
280
+ if (this._ownRoom(msg) && this._phase !== 'deferred') {
281
+ this._cancel('aborted');
282
+ }
283
+ }
284
+
285
+ _onReleased(msg) {
286
+ if (!this._ownRoom(msg)) {
287
+ return;
288
+ }
289
+
290
+ const stay = this._stay;
291
+
292
+ this._finish();
293
+ this._onReleasedCb({ stay, epoch: msg.epoch });
294
+ }
295
+
296
+ // мастер пропал: сорвалась ли передача, узнаем после реконнекта
297
+ // (reclaim_host → staleEpoch, если бета успела). До тех пор матч идёт здесь
298
+ _onSignalingClose() {
299
+ if (this.active) {
300
+ this._cancel('signalingLost', { notify: false });
301
+ }
302
+ }
303
+
304
+ // окончательная отмена: остающийся хост размораживает матч (если
305
+ // замораживал), уходящий уходит всё равно
306
+ _cancel(reason, { notify = true } = {}) {
307
+ const room = this._room;
308
+ const stay = this._stay;
309
+ const frozen = this._phase === 'frozen';
310
+
311
+ this._finish();
312
+
313
+ if (stay) {
314
+ if (frozen) {
315
+ this._getController()?.unfreeze();
316
+ }
317
+
318
+ this._onAbortedCb({ reason, frozen });
319
+ return;
320
+ }
321
+
322
+ if (notify) {
323
+ this._signaling.hostLeaving(room.roomId, room.epoch);
324
+ }
325
+
326
+ this._onLeave({ reason });
327
+ }
328
+
329
+ _finish() {
330
+ this._clearSlowTimer();
331
+
332
+ if (this._phase === 'deferred') {
333
+ this._clearDeferTimer();
334
+ this._getController()?.cancelRoundBoundary();
335
+ }
336
+
337
+ if (this._deadlineTimer !== null) {
338
+ this._timers.clearTimeout(this._deadlineTimer);
339
+ this._deadlineTimer = null;
340
+ }
341
+
342
+ if (this._phase !== null) {
343
+ for (const [type, handler] of SIGNALING_EVENTS) {
344
+ this._signaling.publisher.off(type, handler, this);
345
+ }
346
+ }
347
+
348
+ this._phase = null;
349
+ this._room = null;
350
+ this._reason = null;
351
+ }
352
+
353
+ _clearDeferTimer() {
354
+ if (this._deferTimer !== null) {
355
+ this._timers.clearTimeout(this._deferTimer);
356
+ this._deferTimer = null;
357
+ }
358
+ }
359
+
360
+ _clearSlowTimer() {
361
+ if (this._slowTimer !== null) {
362
+ this._timers.clearTimeout(this._slowTimer);
363
+ this._slowTimer = null;
364
+ }
365
+ }
366
+ }