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.
Files changed (130) 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/ClientReports.js +6 -2
  9. package/src/client/components/model/Controls.js +27 -0
  10. package/src/client/components/model/Games.js +5 -1
  11. package/src/client/components/model/Lobby.js +32 -24
  12. package/src/client/components/model/LobbyAuth.js +20 -4
  13. package/src/client/components/model/RoomMenu.js +121 -0
  14. package/src/client/components/model/Stat.js +2 -1
  15. package/src/client/components/model/Vote.js +122 -4
  16. package/src/client/components/view/Auth.js +20 -10
  17. package/src/client/components/view/ClientReports.js +16 -5
  18. package/src/client/components/view/Games.js +39 -14
  19. package/src/client/components/view/Lobby.js +90 -43
  20. package/src/client/components/view/LobbyAuth.js +13 -7
  21. package/src/client/components/view/Panel.js +1 -2
  22. package/src/client/components/view/RoomMenu.js +69 -0
  23. package/src/client/debug.js +24 -1
  24. package/src/client/lib/FpsMeter.js +46 -0
  25. package/src/client/lib/JoinRetry.js +73 -0
  26. package/src/client/lib/diagnostics.js +28 -5
  27. package/src/client/lib/hostCaps.js +82 -0
  28. package/src/client/lib/hostUnloadGuard.js +108 -0
  29. package/src/client/lib/hostVoteCommand.js +92 -0
  30. package/src/client/lib/pageReload.js +29 -0
  31. package/src/client/lib/roomLink.js +257 -0
  32. package/src/client/lib/signalingErrors.js +74 -0
  33. package/src/client/main.js +674 -451
  34. package/src/client/network/HiddenHostHealthLog.js +76 -0
  35. package/src/client/network/HostConnectionManager.js +62 -8
  36. package/src/client/network/HostController.js +376 -23
  37. package/src/client/network/HostHealthPolicy.js +307 -0
  38. package/src/client/network/HostHealthReporter.js +54 -0
  39. package/src/client/network/HostPrewarm.js +235 -0
  40. package/src/client/network/LoopbackTransport.js +16 -1
  41. package/src/client/network/PlannedHandoff.js +366 -0
  42. package/src/client/network/Promotion.js +420 -0
  43. package/src/client/network/RoomPeersReporter.js +81 -0
  44. package/src/client/network/SessionSupervisor.js +730 -0
  45. package/src/client/network/SignalingClient.js +183 -30
  46. package/src/client/network/StandbyReceiver.js +287 -0
  47. package/src/client/network/StandbySender.js +319 -0
  48. package/src/client/network/TokenHandoffTimer.js +81 -0
  49. package/src/client/network/WebRtcManager.js +338 -11
  50. package/src/client/network/policyClose.js +32 -2
  51. package/src/client/network/standbyChunks.js +185 -0
  52. package/src/client/session/GuestSession.js +428 -0
  53. package/src/client/session/HandoffFlow.js +399 -0
  54. package/src/client/session/HostRole.js +708 -0
  55. package/src/client/session/Membership.js +179 -0
  56. package/src/client/session/PromotionFlow.js +325 -0
  57. package/src/client/session/RouteBoot.js +432 -0
  58. package/src/client/session/StandbyRole.js +195 -0
  59. package/src/client/session/hostRoomPrep.js +152 -0
  60. package/src/client/session/roomContext.js +32 -0
  61. package/src/client/style.css +83 -22
  62. package/src/client/views/gameShell.js +7 -0
  63. package/src/client/views/includes/informer.pug +4 -0
  64. package/src/client/views/includes/lobby.pug +0 -3
  65. package/src/client/views/includes/roomMenu.pug +9 -0
  66. package/src/client/views/index.pug +1 -0
  67. package/src/config/closeCodes.js +1 -1
  68. package/src/config/env.js +37 -0
  69. package/src/config/hostDefaults.js +24 -0
  70. package/src/config/lobby.js +154 -3
  71. package/src/config/master.js +104 -21
  72. package/src/config/wsports.js +8 -0
  73. package/src/devtools/RecordingSocketManager.js +3 -2
  74. package/src/devtools/ScenarioRunner.js +123 -8
  75. package/src/devtools/VirtualClient.js +20 -0
  76. package/src/devtools/contract/format.js +4 -1
  77. package/src/devtools/contract/index.js +1 -2
  78. package/src/devtools/contract/rules/a2-package-scripts.js +1 -3
  79. package/src/devtools/contract/rules/b8-system-messages.js +1 -1
  80. package/src/devtools/contract/rules/b9-vote-names.js +25 -1
  81. package/src/devtools/contract/rules/c11-stat-mode.js +4 -1
  82. package/src/devtools/contract/rules/c6-stat-columns.js +1 -1
  83. package/src/devtools/contract/rules/c7-key-sets.js +8 -4
  84. package/src/devtools/contract/rules/d3-snapshot-interp.js +2 -1
  85. package/src/devtools/contract/rules/e1-sound-pairs.js +3 -1
  86. package/src/devtools/contract/rules/e4-map-layers.js +13 -2
  87. package/src/devtools/contract/rules/e5-map-radar-walls.js +7 -1
  88. package/src/devtools/contract/rules/index.js +38 -5
  89. package/src/devtools/invariants.js +10 -4
  90. package/src/devtools/pluginLoader.js +1 -4
  91. package/src/devtools/report.js +17 -1
  92. package/src/host/DebugRecorder.js +6 -1
  93. package/src/host/GameCoreAdapter.js +45 -0
  94. package/src/host/HostGame.js +928 -59
  95. package/src/host/PortMachine.js +268 -20
  96. package/src/host/host.worker.js +222 -16
  97. package/src/host/meta/SocketManager.js +20 -0
  98. package/src/host/meta/core/RoundManager.js +89 -14
  99. package/src/host/meta/modules/Accolades.js +4 -1
  100. package/src/host/meta/modules/Panel.js +30 -0
  101. package/src/host/meta/modules/PlayerDataSync.js +255 -46
  102. package/src/host/meta/modules/RTTManager.js +34 -0
  103. package/src/host/meta/modules/TimerManager.js +215 -18
  104. package/src/host/meta/modules/chat/systemMessages.js +22 -0
  105. package/src/host/meta/player/HumanParticipant.js +10 -0
  106. package/src/host/meta/player/ParticipantManager.js +98 -5
  107. package/src/lib/AbstractTimer.js +63 -1
  108. package/src/lib/Publisher.js +26 -0
  109. package/src/lib/capabilities.js +5 -0
  110. package/src/lib/checkpointCodec.js +125 -0
  111. package/src/lib/clientIp.js +9 -2
  112. package/src/lib/createHostRuntime.js +95 -6
  113. package/src/lib/formOptions.js +3 -1
  114. package/src/lib/gameConfigView.js +10 -0
  115. package/src/lib/gameId.js +13 -0
  116. package/src/lib/jwt.js +3 -1
  117. package/src/lib/loadGamePackage.js +2 -2
  118. package/src/lib/resumeKey.js +38 -0
  119. package/src/lib/roomId.js +39 -0
  120. package/src/lib/roomSettings.js +59 -0
  121. package/src/lib/validators.js +7 -0
  122. package/tests/fixtures/generations/gen-api3/host/ScriptedManager.js +2 -3
  123. package/tests/fixtures/generations/gen-api3/host/fakeCore.js +31 -5
  124. package/tests/fixtures/generations/gen-api3/manifest.json +1 -3
  125. package/tests/fixtures/generations/gen-api4/host/ScriptedManager.js +2 -3
  126. package/tests/fixtures/generations/gen-api4/host/fakeCore.js +31 -5
  127. package/tests/fixtures/generations/gen-api4/manifest.json +1 -3
  128. package/tests/fixtures/miniGame/config/client.js +15 -0
  129. package/tests/fixtures/miniGame/host/ScriptedManager.js +2 -3
  130. package/tests/fixtures/miniGame/host/fakeCore.js +31 -5
@@ -0,0 +1,152 @@
1
+ // Подготовка комнаты к Worker'у хоста: room.game по манифесту игры, карты
2
+ // мастера, URL worker-бандла. Общая для создания комнаты, прогрева
3
+ // преемника (host-migration этап 6, HostPrewarm) и эстафеты Worker'ов.
4
+ //
5
+ // Без DOM: активную игру, конфиг и сеть инъектирует сборщик
6
+ // (client/main.js).
7
+
8
+ /**
9
+ * @param {Object} deps
10
+ * @param {Function} deps.getActiveGame - () → манифест активной игры.
11
+ * @param {Object} deps.config - lobby-конфиг (maps, worker, game).
12
+ * @param {boolean} deps.isDevBuild
13
+ * @param {Function} deps.fetchGamePluginManifest - lib/gamePlugin.js.
14
+ * @param {Function} [deps.fetch]
15
+ */
16
+ export function createHostRoomPrep({
17
+ getActiveGame,
18
+ config,
19
+ isDevBuild,
20
+ fetchGamePluginManifest,
21
+ fetch = (...args) => globalThis.fetch(...args),
22
+ }) {
23
+ // Этап 5.1/6.4: скачивает каталог карт мастера активной игры (манифест +
24
+ // все карты)
25
+ async function fetchMasterMaps(gameManifest = getActiveGame()) {
26
+ const manifestRes = await fetch(config.maps.manifestUrl(gameManifest));
27
+
28
+ if (!manifestRes.ok) {
29
+ throw new Error(`maps manifest: HTTP ${manifestRes.status}`);
30
+ }
31
+
32
+ const manifest = await manifestRes.json();
33
+
34
+ const entries = await Promise.all(
35
+ manifest.maps.map(async name => {
36
+ const url = `${config.maps.baseUrl(gameManifest)}/${encodeURIComponent(name)}`;
37
+ const res = await fetch(url);
38
+
39
+ if (!res.ok) {
40
+ throw new Error(`map ${name}: HTTP ${res.status}`);
41
+ }
42
+
43
+ return [name, await res.json()];
44
+ }),
45
+ );
46
+
47
+ return { version: manifest.version, maps: Object.fromEntries(entries) };
48
+ }
49
+
50
+ // Этап 5.2: скачивает манифест worker-бандла мастера ({ version, url })
51
+ async function fetchWorkerManifest() {
52
+ const res = await fetch(config.worker.manifestUrl);
53
+
54
+ if (!res.ok) {
55
+ throw new Error(`worker manifest: HTTP ${res.status}`);
56
+ }
57
+
58
+ return res.json();
59
+ }
60
+
61
+ // Этап 6.5: перечитывает манифест активной игры мастера — своп не должен
62
+ // нести новому Worker'у закэшированный с момента создания комнаты
63
+ // hostEntryUrl/wasmUrl (деплой игры мог обновиться независимо от движка)
64
+ async function fetchGameManifest(gameId) {
65
+ return fetchGamePluginManifest(config.game.manifestUrl(gameId));
66
+ }
67
+
68
+ // манифест игры по ссылке на неё: уже загруженный активный — как есть,
69
+ // другая версия — версионный манифест мастера
70
+ async function resolveGameManifest(gameRef) {
71
+ if (gameRef?.entries) {
72
+ return gameRef;
73
+ }
74
+
75
+ const active = getActiveGame();
76
+
77
+ if (
78
+ active &&
79
+ gameRef.id === active.id &&
80
+ gameRef.version === active.version
81
+ ) {
82
+ return active;
83
+ }
84
+
85
+ return fetchGamePluginManifest(
86
+ config.game.versionManifestUrl(gameRef.id, gameRef.version),
87
+ );
88
+ }
89
+
90
+ // gameRef — манифест игры или { id, version } из контрольной точки: бета
91
+ // обязана поднять ту версию игры, что крутится в комнате (версионные URL
92
+ // мастера)
93
+ async function prepareHostRoom(room, gameRef = getActiveGame()) {
94
+ const gameManifest = await resolveGameManifest(gameRef);
95
+
96
+ // отладочный контур (этап 6): рекордер живого матча и хостовый
97
+ // CONSOLE-лог поднимаются только в dev-сборке
98
+ room.isDevMode = isDevBuild;
99
+
100
+ // Этап 6.4: Worker грузит HostPlugin динамически по
101
+ // entries.host/entries.wasm активной игры — движок не знает игру
102
+ // статически
103
+ room.game = {
104
+ id: gameManifest.id,
105
+ version: gameManifest.version,
106
+ hostEntryUrl: gameManifest.entries.host,
107
+ wasmUrl: gameManifest.entries.wasm,
108
+ };
109
+
110
+ // Этап 5.1: комната стартует на актуальных картах мастера;
111
+ // недоступность каталога некритична — Worker возьмёт карты из бандла
112
+ let mapsVersion = null;
113
+
114
+ try {
115
+ const catalog = await fetchMasterMaps(gameManifest);
116
+
117
+ room.maps = catalog.maps;
118
+ mapsVersion = catalog.version;
119
+ } catch (e) {
120
+ console.warn('[maps] master catalog unavailable, using bundled maps:', e);
121
+ }
122
+
123
+ // Этап 5.2: Worker создаётся по манифесту мастера — бандл страницы после
124
+ // деплоя исчезает из раздачи; без манифеста (dev) — бандловый URL,
125
+ // обновления кода отключены
126
+ let workerUrl = null;
127
+ let codeVersion = null;
128
+
129
+ try {
130
+ const manifest = await fetchWorkerManifest();
131
+
132
+ // составной codeVersion (Этап 6.5): движок (worker-бандл) + игра
133
+ // (id/version манифеста, с которым комната стартует)
134
+ codeVersion = {
135
+ engine: manifest.version,
136
+ game: { id: gameManifest.id, version: gameManifest.version },
137
+ };
138
+ workerUrl = manifest.url;
139
+ } catch (e) {
140
+ console.warn('[worker] master manifest unavailable, using bundled:', e);
141
+ }
142
+
143
+ return { room, workerUrl, mapsVersion, codeVersion };
144
+ }
145
+
146
+ return {
147
+ prepareHostRoom,
148
+ fetchMasterMaps,
149
+ fetchWorkerManifest,
150
+ fetchGameManifest,
151
+ };
152
+ }
@@ -0,0 +1,32 @@
1
+ // Общее состояние вкладки в комнате (лобби-режим): его читают и меняют
2
+ // роли хоста, гостя, беты, промоушен и маршруты (client/session/*).
3
+ // Обычный объект, а не класс: модули получают одну и ту же ссылку от
4
+ // сборщика (client/main.js), тесты — свежую на каждый сценарий.
5
+
6
+ /**
7
+ * @returns {{
8
+ * roomId: string|null,
9
+ * epoch: number|null,
10
+ * memberJoined: boolean,
11
+ * entered: boolean,
12
+ * startFailed: boolean,
13
+ * }}
14
+ */
15
+ export function createRoomContext() {
16
+ return {
17
+ // комната, в которой вкладка гость или хост
18
+ roomId: null,
19
+ // эпоха хоста комнаты, ответившего последним: ответы прежних эпох чужие
20
+ epoch: null,
21
+ // вкладка объявила мастеру членство (join_room) и повторяет его после
22
+ // реконнекта сигналинга — даже если WebRTC в этот момент переподключается
23
+ memberJoined: false,
24
+ // вкладка ушла из лобби в комнату (по ссылке или из лобби). Не
25
+ // сбрасывается: после разрыва страница — остатки матча, и новый маршрут
26
+ // из hashchange поднимается перезагрузкой, а не бутстрапом поверх них
27
+ entered: false,
28
+ // Worker комнаты не поднялся: та же быстрая игра создала бы её снова и
29
+ // снова — поэтому уход на главную, как при кике
30
+ startFailed: false,
31
+ };
32
+ }
@@ -86,7 +86,9 @@ body.hide-cursor {
86
86
  left: 0;
87
87
  right: 0;
88
88
  height: 24px;
89
- z-index: 4;
89
+ /* 7 — выше прозрачного #game-informer (6), иначе тот перехватывал бы
90
+ клики по кнопке меню комнаты; ниже лобби/экрана авторизации игры (8) */
91
+ z-index: 7;
90
92
  overflow: hidden;
91
93
  }
92
94
 
@@ -359,6 +361,58 @@ body.hide-cursor {
359
361
  display: block;
360
362
  }
361
363
 
364
+ /*room menu (host-migration, этап 3): кнопка внутри #panel справа от
365
+ таблицы (RoomMenuView.placeInPanel); order — таблицу PanelView достраивает
366
+ позже, по CONFIG_DATA. Без панели (игра её не показывает) — угол экрана;
367
+ z-index 7 — выше прозрачного #game-informer (6) и ниже лобби (8)*/
368
+
369
+ #room-menu {
370
+ position: absolute;
371
+ top: 0;
372
+ right: 0;
373
+ z-index: 7;
374
+ font-size: 12px;
375
+ text-transform: none;
376
+ }
377
+
378
+ #panel > #room-menu {
379
+ position: static;
380
+ order: 1;
381
+ flex: 0 0 auto;
382
+ }
383
+
384
+ #room-menu-toggle {
385
+ display: block;
386
+ margin-left: auto;
387
+ height: 24px;
388
+ padding: 0 8px;
389
+ border: none;
390
+ background: transparent;
391
+ }
392
+
393
+ /* fixed — иначе список обрезал бы overflow: hidden панели */
394
+ #room-menu-list {
395
+ position: fixed;
396
+ top: 24px;
397
+ right: 0;
398
+ padding: 4px;
399
+ background: #fff;
400
+ }
401
+
402
+ #room-menu-list input {
403
+ display: block;
404
+ width: 100%;
405
+ white-space: nowrap;
406
+ margin-top: 5px;
407
+ }
408
+
409
+ #room-menu-status {
410
+ padding: 2px 4px;
411
+ /* тёмный: фон списка белый */
412
+ color: #333;
413
+ white-space: nowrap;
414
+ }
415
+
362
416
  /*chat*/
363
417
 
364
418
  #chat {
@@ -633,6 +687,25 @@ body.hide-cursor {
633
687
  white-space: pre-line;
634
688
  }
635
689
 
690
+ /* оверлей сессии (host-migration этап 4): матч на паузе, пока клиент
691
+ переподключается к хосту; уровень лобби/авторизации — они с ним не
692
+ показываются одновременно, а #tech-informer остаётся выше. Своего
693
+ display нет: скрыт он `.vimp-shell > *`, показывает его main.js (flex) */
694
+ #session-overlay {
695
+ position: absolute;
696
+ top: 0;
697
+ left: 0;
698
+ z-index: 8;
699
+ align-items: center;
700
+ justify-content: center;
701
+ width: 100%;
702
+ height: 100%;
703
+ background-color: rgb(0 0 0 / 60%);
704
+ color: #fff;
705
+ font-size: 20px;
706
+ line-height: 24px;
707
+ }
708
+
636
709
  #game-informer {
637
710
  position: absolute;
638
711
  top: 0;
@@ -700,13 +773,6 @@ body.hide-cursor {
700
773
  gap: 10px;
701
774
  }
702
775
 
703
- /* поле имени сервера — строка формы, а не узкий числовой инпут
704
- (.form-row input[type='text'] рассчитан на счётчики вроде max players) */
705
- #lobby-name {
706
- width: 200px;
707
- text-align: left;
708
- }
709
-
710
776
  #lobby input[type='text'],
711
777
  #lobby select {
712
778
  box-sizing: border-box;
@@ -741,7 +807,7 @@ body.hide-cursor {
741
807
 
742
808
  .lobby-card {
743
809
  display: grid;
744
- grid-template-columns: 1fr auto auto;
810
+ grid-template-columns: 1fr auto;
745
811
  column-gap: 8px;
746
812
  row-gap: 2px;
747
813
  padding: 10px 12px;
@@ -758,13 +824,19 @@ body.hide-cursor {
758
824
  }
759
825
 
760
826
  .lobby-card-name {
761
- grid-column: 1 / -1;
827
+ grid-column: 1;
762
828
  font-weight: bold;
763
829
  overflow: hidden;
764
830
  text-overflow: ellipsis;
765
831
  white-space: nowrap;
766
832
  }
767
833
 
834
+ .lobby-card-copy {
835
+ grid-column: 2;
836
+ font-size: 10px;
837
+ padding: 0 6px;
838
+ }
839
+
768
840
  .lobby-card-info {
769
841
  grid-column: 1;
770
842
  color: #aaa;
@@ -774,19 +846,8 @@ body.hide-cursor {
774
846
  white-space: nowrap;
775
847
  }
776
848
 
777
- .lobby-card-rating {
778
- grid-column: 2;
779
- text-align: right;
780
- color: #4caf50;
781
- font-size: 10px;
782
- }
783
-
784
- .lobby-card-rating.is-negative {
785
- color: #e57373;
786
- }
787
-
788
849
  .lobby-card-latency {
789
- grid-column: 3;
850
+ grid-column: 2;
790
851
  text-align: right;
791
852
  color: #ff0;
792
853
  font-size: 10px;
@@ -67,6 +67,13 @@ const SHELL = [
67
67
  ],
68
68
  },
69
69
  { tag: 'div', id: 'game-informer' },
70
+ // оверлей переподключения (host-migration этап 4): в solo/dedicated не
71
+ // показывается, но разметка общая с informer.pug
72
+ {
73
+ tag: 'div',
74
+ id: 'session-overlay',
75
+ children: [{ tag: 'span', id: 'session-overlay-text' }],
76
+ },
70
77
  { tag: 'div', id: 'tech-informer' },
71
78
  ];
72
79
 
@@ -1,2 +1,6 @@
1
1
  div#game-informer
2
+ //- оверлей сессии (host-migration этап 4): «Reconnecting…» поверх матча на
3
+ //- время переподключения к хосту; терминальный #tech-informer — выше него
4
+ div#session-overlay
5
+ span#session-overlay-text
2
6
  div#tech-informer
@@ -31,9 +31,6 @@ div#lobby(style='display:none')
31
31
  div.form-row
32
32
  label.form-label(for='lobby-game')= 'Select game'
33
33
  select#lobby-game.field-select
34
- div.form-row
35
- label.form-label(for='lobby-name')= 'Server name'
36
- input#lobby-name.field-text(type='text', maxlength='30', autocomplete='off')
37
34
  //- поля настроек комнаты генерируются по явной схеме roomForm
38
35
  //- манифеста активной игры (docs/en/plugin-api.md "Form schema")
39
36
  div#lobby-fields
@@ -0,0 +1,9 @@
1
+ //- меню комнаты (host-migration, этапы 3 и 8d): только лобби-режим, пока
2
+ //- вкладка в комнате. «Hand over host» — только у хоста, когда передавать
3
+ //- есть кому; статус передачи — на месте заблокированных пунктов
4
+ div#room-menu(style='display:none')
5
+ input#room-menu-toggle(type='button', value='☰', title='Room menu')
6
+ div#room-menu-list(style='display:none')
7
+ input#room-menu-handover(type='button', value='Hand over host', style='display:none')
8
+ input#room-menu-leave(type='button', value='Leave server')
9
+ div#room-menu-status(style='display:none')
@@ -2,6 +2,7 @@
2
2
  include includes/panel
3
3
  include includes/chat
4
4
  include includes/stat
5
+ include includes/roomMenu
5
6
  include includes/lobbyAuth
6
7
  include includes/lobby
7
8
  include includes/games
@@ -7,7 +7,7 @@
7
7
  export default {
8
8
  staleHost: 4000, // master/SignalingServer.js — сигнальный WS хоста протух
9
9
  invalidOrigin: 4001, // master/SignalingServer.js, dedicated/main.js
10
- blocked: 4002, // master/SignalingServer.js — хостер заблокирован рейтингом
10
+ // 4002 retired (server rating removed) — номер не переиспользуется
11
11
  kickForMaxLatency: 4003, // host/HostGame.js
12
12
  kickForMissedPings: 4004, // host/HostGame.js
13
13
  kickIdle: 4005, // host/HostGame.js
package/src/config/env.js CHANGED
@@ -39,6 +39,43 @@ export function applyMasterEnv(config, env = process.env) {
39
39
  }
40
40
  }
41
41
 
42
+ // минимальная длина VIMP_ROOM_SECRET_KEY — ключ HMAC-SHA256
43
+ const ROOM_SECRET_KEY_MIN_BYTES = 32;
44
+
45
+ /**
46
+ * Ключ секрета комнаты (host-migration, этап 2): roomSecret = HMAC(ключ,
47
+ * roomId:epoch:hostUserId) — им хост доказывает комнату после рестарта
48
+ * мастера. В production обязателен; в dev без него генерируется случайный на
49
+ * время процесса (комнаты после рестарта dev-мастера не восстанавливаются).
50
+ * @param {Object} [env] - Окружение (по умолчанию process.env).
51
+ * @param {Object} opts
52
+ * @param {boolean} opts.isProduction
53
+ * @param {Function} opts.randomBytes - crypto.randomBytes.
54
+ * @returns {{ key: string|Buffer, ephemeral: boolean }}
55
+ */
56
+ export function readRoomSecretKey(
57
+ env = process.env,
58
+ { isProduction, randomBytes },
59
+ ) {
60
+ const key = env.VIMP_ROOM_SECRET_KEY;
61
+
62
+ if (key) {
63
+ if (Buffer.byteLength(key) < ROOM_SECRET_KEY_MIN_BYTES) {
64
+ throw new Error(
65
+ `VIMP_ROOM_SECRET_KEY: at least ${ROOM_SECRET_KEY_MIN_BYTES} bytes required`,
66
+ );
67
+ }
68
+
69
+ return { key, ephemeral: false };
70
+ }
71
+
72
+ if (isProduction) {
73
+ throw new Error('VIMP_ROOM_SECRET_KEY must be set in production');
74
+ }
75
+
76
+ return { key: randomBytes(ROOM_SECRET_KEY_MIN_BYTES), ephemeral: true };
77
+ }
78
+
42
79
  /**
43
80
  * Настройки комнаты dedicated-сервера: VIMP_DEDICATED_ROOM — JSON-объект
44
81
  * (map, maxPlayers, roundTime, mapTime, friendlyFire, seed). Мусор в
@@ -64,6 +64,30 @@ export default {
64
64
  maxLatency: 1000,
65
65
  },
66
66
 
67
+ // возобновление сессии (host-migration этап 4). Сколько хост держит место
68
+ // участника, чей транспорт оборвался без LEAVE: слот занят, актор в мире,
69
+ // клавиши отпущены. Включает его только Worker лобби — dedicated и
70
+ // standalone поднимают PortMachine без этой опции (0 — снять сразу)
71
+ resumeGraceMs: 20000,
72
+
73
+ // сколько возобновляющее соединение может молчать до RESUME_REQUEST
74
+ resumeRequestTimeoutMs: 5000,
75
+
76
+ // промоушен преемника (host-migration этап 7.4): сколько матч, поднятый из
77
+ // контрольной точки, стоит на паузе, ожидая возврата людей точки, прежде
78
+ // чем стартовать без них (их место держится ещё resumeGraceMs)
79
+ resumeWaitMs: 3000,
80
+
81
+ // контрольная точка хоста (host-migration этап 5). Предел распакованного
82
+ // размера чужой точки — защита от «zip-бомбы»: сырой дамп tanks ~420 КБ,
83
+ // запас на порядок с лишним
84
+ maxCheckpointBytes: 8 * 1024 * 1024,
85
+
86
+ // сколько эстафета Worker'ов ждёт финальной синхронизации профилей
87
+ // (flushAll) перед отдачей состояния новому Worker'у; дольше — отдаёт
88
+ // как есть, неотправленное едет в состоянии
89
+ handoffFlushTimeoutMs: 3000,
90
+
67
91
  // порог допустимого бездействия перед киком
68
92
  // если null — кик отключен
69
93
  idleKickTimeout: {
@@ -5,6 +5,39 @@ export default {
5
5
  // REST-эндпоинт мастера со списком серверов (GET /servers)
6
6
  serversUrl: '/servers',
7
7
 
8
+ // комната по прямому id (GET /rooms/:roomId, host-migration этап 3):
9
+ // прямая ссылка #/<gameId>/<roomId> проверяется до входа
10
+ roomUrl: roomId => `/rooms/${encodeURIComponent(roomId)}`,
11
+
12
+ // быстрая игра (GET /quickplay/:gameId, ревью F16): лучшая комната игры
13
+ // от мастера вместо всего списка; недоступен — GET /servers?search
14
+ quickPlayUrl: gameId => `/quickplay/${encodeURIComponent(gameId)}`,
15
+
16
+ // быстрая игра по ссылке #/<gameId>: подходящей комнаты нет — создать
17
+ // свою с настройками формы по умолчанию (false — показать лобби этой игры);
18
+ // createDelayMinMs…createDelayMaxMs — случайная пауза перед созданием и
19
+ // повторный GET /servers (host-migration этап 7): гости закрытой комнаты
20
+ // уходят в быструю игру разом, и без паузы каждый создал бы свою
21
+ quickPlay: {
22
+ autoCreate: true,
23
+ createDelayMinMs: 500,
24
+ createDelayMaxMs: 2000,
25
+ },
26
+
27
+ // меню комнаты внутри матча (только лобби-режим, roomMenu.pug)
28
+ roomMenu: {
29
+ elems: {
30
+ // кнопка меню живёт в панели, справа от таблицы
31
+ panelId: 'panel',
32
+ menuId: 'room-menu',
33
+ toggleId: 'room-menu-toggle',
34
+ listId: 'room-menu-list',
35
+ leaveId: 'room-menu-leave',
36
+ handoverId: 'room-menu-handover',
37
+ statusId: 'room-menu-status',
38
+ },
39
+ },
40
+
8
41
  // каталог игр мастера (Этап 6.3, GameCatalog): roomDefaults формы создания
9
42
  // комнаты и ClientPlugin берутся отсюда вместо статической композиции
10
43
  gamesManifestUrl: '/games/manifest.json',
@@ -31,6 +64,10 @@ export default {
31
64
  // (деплой игры мог обновиться независимо от деплоя движка)
32
65
  game: {
33
66
  manifestUrl: gameId => `/games/${gameId}/manifest.json`,
67
+ // манифест конкретной версии: прогрев преемника (host-migration этап 6)
68
+ // поднимает ту версию игры, что крутится в комнате
69
+ versionManifestUrl: (gameId, version) =>
70
+ `/games/${gameId}/${encodeURIComponent(version)}/manifest.json`,
34
71
  },
35
72
 
36
73
  // манифест worker-бандла мастера (Этап 5.2): Worker комнаты создаётся по
@@ -141,6 +178,123 @@ export default {
141
178
  maxDelay: 30000,
142
179
  },
143
180
 
181
+ // установка P2P-соединения с хостом (host-migration этап 4): каналы не
182
+ // открылись за connectTimeoutMs — попытка считается провалившейся;
183
+ // offerRetryMs — пауза перед повтором оффера, отклонённого мастером на
184
+ // время плановой передачи хоста (error migrating, этап 8)
185
+ webrtc: {
186
+ connectTimeoutMs: 10000,
187
+ offerRetryMs: 1000,
188
+ },
189
+
190
+ // миграция хоста (host-migration этап 6; значения — замеры этапа 0):
191
+ // checkpointIntervalMs — период контрольных точек хоста для беты (поток
192
+ // ≤ ~150 КБ/с, serialize ≤ 25 % бюджета тика); standbyChunkBytes — кусок
193
+ // точки в канале standby (≤ минимального maxMessageSize с запасом);
194
+ // standbyHighWaterBytes — bufferedAmount, выше которого периодическая
195
+ // точка пропускается; standbyStatusIntervalMs — как часто бета сообщает
196
+ // мастеру свою последнюю точку (с её возрастом); maxRestoreAgeMs — точку,
197
+ // полученную раньше, преемник не поднимает (promote_failed → холодный
198
+ // старт; больше master:room:checkpointMaxAgeMs на дорогу статуса);
199
+ // standbyReopenDelayMs…standbyReopenMaxDelayMs — экспоненциальная пауза
200
+ // перед повторным открытием канала standby, закрывшегося при живом пире
201
+ // беты; finalWaitMs — сколько бета при плановой
202
+ // передаче (этап 8) ждёт финальную точку замороженного хоста, прежде чем
203
+ // взять последнюю периодическую; handoffSlowMs — нет handoff_go за это
204
+ // время — «медленная связь» (передача продолжается); handoffDeadlineMs —
205
+ // общий дедлайн плановой передачи от handoff_begin (master:room:
206
+ // handoffTimeoutMs + запас на дорогу ответа мастера); deferMaxMs — потолок
207
+ // ожидания границы раунда передачей в игре без migration.midRound (этап 8d);
208
+ // peersReportIntervalMs — период повтора room_peers хоста мастеру (кто
209
+ // подключён по WebRTC; при смене состава отчёт уходит сразу, с дебаунсом);
210
+ // minTokenLifetimeMs — вкладка, чей вход истечёт раньше, не объявляет
211
+ // canHost и отказывается от промоушена (токен предъявляется мастеру
212
+ // посреди матча, продления нет); tokenHandoffLeadMs — за столько до
213
+ // истечения входа хост с бетой сам передаёт роль (плановая передача на
214
+ // границе раунда); tokenHandoffRetryMs — через сколько повторить её, если
215
+ // беты ещё нет, идёт эстафета Worker'ов или передача сорвалась (повторы —
216
+ // до истечения входа); leaveFlushTimeoutMs — сколько «Leave
217
+ // server» хоста без людей ждёт записи очков участников перед закрытием
218
+ // комнаты (HostController.shutdown)
219
+ migration: {
220
+ checkpointIntervalMs: 500,
221
+ standbyChunkBytes: 65536,
222
+ standbyHighWaterBytes: 1024 * 1024,
223
+ standbyStatusIntervalMs: 5000,
224
+ maxRestoreAgeMs: 15000,
225
+ standbyReopenDelayMs: 1000,
226
+ standbyReopenMaxDelayMs: 10000,
227
+ finalWaitMs: 3000,
228
+ handoffSlowMs: 3000,
229
+ handoffDeadlineMs: 10000,
230
+ deferMaxMs: 30000,
231
+ peersReportIntervalMs: 15000,
232
+ minTokenLifetimeMs: 600000,
233
+ tokenHandoffLeadMs: 300000,
234
+ tokenHandoffRetryMs: 5000,
235
+ leaveFlushTimeoutMs: 3000,
236
+
237
+ // автотриггеры передачи (этап 9b, HostHealthPolicy; сэмпл — сообщение
238
+ // health Worker'а раз в ~1 с): мягкая перегрузка — среднее tickRate за
239
+ // overloadWindowMs ниже overloadTickRate (передача ждёт границы раунда);
240
+ // жёсткая — среднее за criticalWindowMs ниже criticalTickRate или
241
+ // lostMs > 0 у lostWindows сэмплов подряд (сразу); отложенная
242
+ // отменяется, когда все сэмплы за recoverWindowMs выше recoverTickRate
243
+ // (гистерезис); hiddenHandoffMs — скрытая вкладка хоста (этап 0:
244
+ // троттлинг Worker'а за 1–3 с); autoHandoffCooldownMs — между
245
+ // авто-передачами вкладки; minHostTenureMs — не отдавать роль, только
246
+ // что её получив; enabled — общий выключатель
247
+ auto: {
248
+ enabled: true,
249
+ overloadTickRate: 100,
250
+ overloadWindowMs: 5000,
251
+ criticalTickRate: 60,
252
+ criticalWindowMs: 3000,
253
+ lostWindows: 3,
254
+ recoverTickRate: 110,
255
+ recoverWindowMs: 5000,
256
+ hiddenHandoffMs: 1500,
257
+ autoHandoffCooldownMs: 90000,
258
+ minHostTenureMs: 30000,
259
+ // этап 9c: host_health мастеру (правило сетевого лага) не чаще
260
+ // hostHealthIntervalMs; гость шлёт свой FPS рендера (caps.fps) раз в
261
+ // fpsReportIntervalMs. Просьбу мастера request_handoff хост
262
+ // выполняет, только если enabled
263
+ hostHealthIntervalMs: 2000,
264
+ fpsReportIntervalMs: 10000,
265
+ },
266
+ },
267
+
268
+ // супервизор сессии гостя (host-migration этап 4, SessionSupervisor):
269
+ // reconnectWindowMs — сколько после обрыва транспорта пытаться вернуться в
270
+ // матч (повторы с бэкоффом reconnectBaseDelayMs…reconnectMaxDelayMs);
271
+ // hostSilenceMs — молчание хоста в игре, после которого транспорт
272
+ // считается мёртвым (кадры идут ~30/с, PING — раз в 3 с);
273
+ // migrationWaitMs — сколько после host_migrating ждать host_changed, затем
274
+ // комната считается закрытой (быструю игру); migrationPollMs — период
275
+ // повторного GET /rooms/:roomId, пока комната по ссылке в 'migrating';
276
+ // host_migrating.waitMs мастера (сколько он ещё ищет преемника) ожидание
277
+ // продлевает, но не укорачивает; linkWaitMaxMs — крайний срок ожидания
278
+ // комнаты по ссылке, пока она меняет хоста; resumeSilenceGraceMs — фора
279
+ // сторожку тишины после возобновления до первого кадра: восстановленный
280
+ // матч стоит до hostDefaults.resumeWaitMs, ожидая остальных. Связки с
281
+ // таймингами мастера и Worker'а — tests/config/migrationTimings.test.js;
282
+ // joinRetryWindowMs — сколько гость повторяет join_room на unknownRoom
283
+ // после рестарта мастера, пока хост не вернёт комнату reclaim_host (больше
284
+ // reconnect.maxDelay — бэкоффа сигналинга хоста — с запасом на его
285
+ // reclaim_host)
286
+ session: {
287
+ reconnectWindowMs: 15000,
288
+ reconnectBaseDelayMs: 500,
289
+ reconnectMaxDelayMs: 4000,
290
+ hostSilenceMs: 3000,
291
+ migrationWaitMs: 40000,
292
+ migrationPollMs: 1000,
293
+ linkWaitMaxMs: 90000,
294
+ resumeSilenceGraceMs: 3000,
295
+ joinRetryWindowMs: 45000,
296
+ },
297
+
144
298
  // приёмник выгрузок отладочного контура (этап 6 плана plan/done/ai-debug):
145
299
  // маршрут поднимается мастером только в dev, в проде вернёт 404
146
300
  debugReportUrl: '/debug/report',
@@ -163,7 +317,6 @@ export default {
163
317
  searchId: 'lobby-search',
164
318
  moreId: 'lobby-more',
165
319
  emptyId: 'lobby-empty',
166
- nameId: 'lobby-name',
167
320
  hostBtnId: 'lobby-host',
168
321
  // строка отказа под кнопкой: загрузка ClientPlugin выбранной игры может
169
322
  // не удаться, и лобби обязано остаться рабочим
@@ -327,8 +480,6 @@ export default {
327
480
  // раунда-карты/огонь по своим/карта по умолчанию — из roomDefaults
328
481
  // манифеста активной игры (Этап 6.3), не бандлятся здесь
329
482
  create: {
330
- defaultName: 'My Server',
331
-
332
483
  // каталог платформы пуст: реестр ещё ничего не одобрил либо модератор
333
484
  // снял с раздачи последнюю игру. Комнату создавать не на чем, но лобби
334
485
  // живо — и текст называет то единственное, что выводит его из этого