vimp-engine 0.24.2 → 0.26.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.
@@ -35,8 +35,13 @@ import { normalizeAuthParams } from './lib/authParams.js';
35
35
  import { renderProjectLink } from './lib/footerLink.js';
36
36
  import { createGameActivator } from './lib/gameActivator.js';
37
37
  import createAutostart from './lib/autostart.js';
38
+ import { applyCatalogState } from './lib/catalogState.js';
38
39
  import { getBootConfig, resolveBootConfig } from './boot.js';
39
- import { ensureGameShell, ensureCanvas } from './views/gameShell.js';
40
+ import {
41
+ ensureGameShell,
42
+ ensureCanvas,
43
+ showBootFailure,
44
+ } from './views/gameShell.js';
40
45
  import { createContextTracker } from './lib/contextTracker.js';
41
46
  import { createLocalPlayer } from './lib/localPlayer.js';
42
47
  import { createAccolades } from './lib/accolades.js';
@@ -73,6 +78,9 @@ import LobbyCtrl from './components/controller/Lobby.js';
73
78
  import LobbyAuthModel from './components/model/LobbyAuth.js';
74
79
  import LobbyAuthView from './components/view/LobbyAuth.js';
75
80
  import LobbyAuthCtrl from './components/controller/LobbyAuth.js';
81
+ import GamesModel from './components/model/Games.js';
82
+ import GamesView from './components/view/Games.js';
83
+ import GamesCtrl from './components/controller/Games.js';
76
84
  import BakingProvider from './providers/BakingProvider.js';
77
85
  import DependencyProvider from './providers/DependencyProvider.js';
78
86
  import { HOT_FLAGS } from '../config/opcodes.js';
@@ -99,6 +107,10 @@ let activeGameManifest;
99
107
  let clientPlugin;
100
108
  let gamesManifest;
101
109
 
110
+ // почему активной игры нет, если каталог непуст: строка отказа лобби вместо
111
+ // общего «игр пока не опубликовано» (см. pickActiveGame)
112
+ let catalogProblem = null;
113
+
102
114
  // узел игрового CSS: при переключении игры его текст заменяется, иначе стили
103
115
  // двух игр жили бы в head одновременно и конфликтовали селекторами
104
116
  let gameStyleNode = null;
@@ -154,20 +166,40 @@ try {
154
166
  throw gamesManifest;
155
167
  }
156
168
 
157
- // недоступная игра активной быть не может (lib/pickActiveGame.js)
158
- activeGameManifest = pickActiveGame(gamesManifest, boot.gameId);
169
+ try {
170
+ // недоступная игра активной быть не может (lib/pickActiveGame.js)
171
+ activeGameManifest = pickActiveGame(gamesManifest, boot.gameId);
172
+ } catch (e) {
173
+ // «каталог непустой, но играбельного в нём нет» (движок обновили, все
174
+ // опубликованные игры просят возможность, которой в нём уже нет) — для
175
+ // лобби это то же самое, что пустой каталог: играть не во что, но
176
+ // модерация и заявка на месте, и только они это чинят. Терять их из-за
177
+ // такого нельзя, поэтому причина едет строкой отказа лобби, а не
178
+ // терминальным оверлеем. В solo/dedicated отказ остаётся отказом
179
+ if (!isLobbyMode) {
180
+ throw e;
181
+ }
182
+
183
+ catalogProblem = e.message;
184
+ }
159
185
  }
160
186
 
161
- if (!activeGameManifest) {
187
+ // пустой каталог — состояние лобби, а не отказ загрузки: модератор вправе
188
+ // отключить последнюю игру, реестр вправе ещё ничего не одобрить, и вернуть
189
+ // каталог к жизни можно только из панели, которая живёт в этой же вкладке.
190
+ // В solo/dedicated игры нет — это отказ: там весь смысл вкладки в матче
191
+ if (!activeGameManifest && !isLobbyMode) {
162
192
  throw new Error('master has no games in its catalog');
163
193
  }
164
194
 
165
- bindActiveGame(
166
- activeGameManifest,
167
- boot.clientPlugin ?? (await loadClientPlugin(activeGameManifest)),
168
- );
195
+ if (activeGameManifest) {
196
+ bindActiveGame(
197
+ activeGameManifest,
198
+ boot.clientPlugin ?? (await loadClientPlugin(activeGameManifest)),
199
+ );
200
+ }
169
201
  } catch (e) {
170
- document.body.textContent = `Failed to load the game: ${e.message}`;
202
+ showBootFailure(`Failed to load the game: ${e.message}`, gameContainer);
171
203
  throw e;
172
204
  }
173
205
 
@@ -1718,8 +1750,7 @@ let hostRegistration = null;
1718
1750
  // Этап 5.1/6.4: скачивает каталог карт мастера активной игры (манифест +
1719
1751
  // все карты)
1720
1752
  async function fetchMasterMaps() {
1721
- const gameId = activeGameManifest.id;
1722
- const manifestRes = await fetch(lobbyConfig.maps.manifestUrl(gameId));
1753
+ const manifestRes = await fetch(lobbyConfig.maps.manifestUrl(activeGameManifest));
1723
1754
 
1724
1755
  if (!manifestRes.ok) {
1725
1756
  throw new Error(`maps manifest: HTTP ${manifestRes.status}`);
@@ -1729,7 +1760,7 @@ async function fetchMasterMaps() {
1729
1760
 
1730
1761
  const entries = await Promise.all(
1731
1762
  manifest.maps.map(async name => {
1732
- const url = `${lobbyConfig.maps.baseUrl(gameId)}/${encodeURIComponent(name)}`;
1763
+ const url = `${lobbyConfig.maps.baseUrl(activeGameManifest)}/${encodeURIComponent(name)}`;
1733
1764
  const res = await fetch(url);
1734
1765
 
1735
1766
  if (!res.ok) {
@@ -1840,8 +1871,16 @@ async function fetchServers({ offset, limit, search }) {
1840
1871
  params.set('limit', limit);
1841
1872
  }
1842
1873
 
1874
+ // токен едет и сюда (master-game-registry, этап 4): список серверов
1875
+ // публичный, но админу мастер отдаёт вместе с ним скрытые тестовые
1876
+ // комнаты застейдженных версий
1877
+ const token = lobbyAuthModel?.getToken();
1878
+
1843
1879
  try {
1844
- const res = await fetch(`${lobbyConfig.serversUrl}?${params}`);
1880
+ const res = await fetch(
1881
+ `${lobbyConfig.serversUrl}?${params}`,
1882
+ token ? { headers: { authorization: `Bearer ${token}` } } : undefined,
1883
+ );
1845
1884
 
1846
1885
  return res.ok ? await res.json() : null;
1847
1886
  } catch (e) {
@@ -1939,6 +1978,11 @@ function populateRoomForm(manifest) {
1939
1978
  }
1940
1979
  }
1941
1980
 
1981
+ // роли, которым лобби показывает кнопку «Moderation» (master-game-registry,
1982
+ // этап 4). Это подсказка интерфейсу: доступ к данным проверяет мастер, а
1983
+ // запись — auth-сервис, перечитывая роль из БД
1984
+ const ADMIN_ROLES = ['admin', 'superadmin'];
1985
+
1942
1986
  // каталог манифестов по id: форма и leaderboard селектора игр, а также
1943
1987
  // активация игры перед созданием комнаты и входом в чужую
1944
1988
  const gamesById = new Map(
@@ -1972,7 +2016,15 @@ function clearLobbyError() {
1972
2016
  // активирует игру и перепривязывает манифест/плагин/CSS; false — отказ уже
1973
2017
  // показан игроку, лобби остаётся рабочим
1974
2018
  async function selectActiveGame(gameId) {
1975
- if (gameId === activeGameManifest.id) {
2019
+ // сверка идёт с самим манифестом, а не с его id: каталог вкладки может
2020
+ // пополниться застейдженной версией той же игры (registerGameManifest), и
2021
+ // сравнение по id оставило бы активной одобренную версию
2022
+ // activeGameManifest?. — каталог платформы может быть пуст: активной игры
2023
+ // у вкладки тогда нет вовсе, и первая же активация обязана состояться
2024
+ if (
2025
+ gameId === activeGameManifest?.id &&
2026
+ gamesById.get(gameId) === activeGameManifest
2027
+ ) {
1976
2028
  return true;
1977
2029
  }
1978
2030
 
@@ -1993,8 +2045,39 @@ async function selectActiveGame(gameId) {
1993
2045
  }
1994
2046
  }
1995
2047
 
2048
+ // очередь активации черновиков (см. подписку на 'staged' в initLobby)
2049
+ let stagedActivation = Promise.resolve();
2050
+
2051
+ // id игр, чей манифест в каталоге вкладки подменён застейдженной версией
2052
+ // (master-game-registry, этап 4): в селекторе такая игра помечается, чтобы
2053
+ // админ видел, что поднимет комнату НЕ на одобренной версии
2054
+ const stagedGameIds = new Set();
2055
+
2056
+ /**
2057
+ * Кладёт манифест застейдженной (не одобренной) версии в каталог вкладки —
2058
+ * так админ может поднять по нему комнату, не трогая каталог игроков.
2059
+ * Дальше всё работает существующим путём: createGameActivator грузит
2060
+ * ClientPlugin по entries.client (ключ кеша версионный, поэтому плагин
2061
+ * черновика не подменит одобренный), connectAsHost поднимает комнату, а
2062
+ * мастер помечает её скрытой по версии манифеста.
2063
+ * @param {Object} manifest - Манифест застейдженной версии из /admin/games.
2064
+ * @returns {void}
2065
+ */
2066
+ function registerGameManifest(manifest) {
2067
+ if (!manifest?.id) {
2068
+ return;
2069
+ }
2070
+
2071
+ gamesById.set(manifest.id, manifest);
2072
+ stagedGameIds.add(manifest.id);
2073
+ populateGameSelect();
2074
+ }
2075
+
1996
2076
  // заполняет #lobby-game всем каталогом манифестов мастера — раньше туда
1997
- // попадал только gamesManifest[0], теперь селектор рабочий
2077
+ // попадал только gamesManifest[0], теперь селектор рабочий.
2078
+ // Источник — gamesById, а не gamesManifest: каталог вкладки пополняется на
2079
+ // лету застейдженными версиями (registerGameManifest), и селектор
2080
+ // перерисовывается целиком
1998
2081
  function populateGameSelect() {
1999
2082
  const gameSelect = document.getElementById(lobbyConfig.elems.gameId);
2000
2083
 
@@ -2002,7 +2085,13 @@ function populateGameSelect() {
2002
2085
  return;
2003
2086
  }
2004
2087
 
2005
- gamesManifest.forEach(manifest => {
2088
+ // перерисовка не должна сбрасывать выбор игрока: селектор целиком
2089
+ // пересобирается и при добавлении застейдженной версии
2090
+ const selected = gameSelect.value;
2091
+
2092
+ gameSelect.textContent = '';
2093
+
2094
+ gamesById.forEach(manifest => {
2006
2095
  const option = document.createElement('option');
2007
2096
  const available = isGameAvailable(manifest);
2008
2097
 
@@ -2011,14 +2100,36 @@ function populateGameSelect() {
2011
2100
  // каталога, и игрок видел пустое лобби без причины), но выбрать её
2012
2101
  // нельзя: комнату по ней всё равно не поднять
2013
2102
  option.textContent = available
2014
- ? manifest.title
2103
+ ? `${manifest.title}${stagedGameIds.has(manifest.id) ? lobbyConfig.games.stagedSuffix : ''}`
2015
2104
  : `${manifest.title} — unavailable`;
2016
2105
  option.disabled = !available;
2017
2106
  option.title = available ? '' : manifest.compat.text;
2018
2107
  gameSelect.appendChild(option);
2019
2108
  });
2020
2109
 
2021
- gameSelect.value = activeGameManifest.id;
2110
+ if (selected && gamesById.has(selected)) {
2111
+ gameSelect.value = selected;
2112
+ } else if (activeGameManifest) {
2113
+ gameSelect.value = activeGameManifest.id;
2114
+ }
2115
+ // без активной игры выбор не трогаем: каталог был пуст, и селектор только
2116
+ // что получил первую строку — она и стоит выбранной по умолчанию.
2117
+ // Присваивание `''` вместо этого сняло бы выбор совсем (selectedIndex = -1)
2118
+ }
2119
+
2120
+ // форма комнаты, Leaderboard и «Create server» по активной игре; пустой
2121
+ // каталог — вторая законная ветка (client/lib/catalogState.js)
2122
+ function syncCatalogState() {
2123
+ applyCatalogState(activeGameManifest, {
2124
+ hostBtn: document.getElementById(lobbyConfig.elems.hostBtnId),
2125
+ emptyText: catalogProblem ?? lobbyConfig.create.emptyCatalogText,
2126
+ bindGame: manifest => {
2127
+ populateRoomForm(manifest);
2128
+ lobby.gameChanged(manifest.id, manifest.title);
2129
+ },
2130
+ showError: showLobbyError,
2131
+ clearError: clearLobbyError,
2132
+ });
2022
2133
  }
2023
2134
 
2024
2135
  // поднимает лобби после welcome от мастера (iceServers уже получены);
@@ -2064,7 +2175,15 @@ function initLobby() {
2064
2175
  // хостов старше 6.4 — тогда заходим на активной игре, как раньше: неизвестная
2065
2176
  // версия не повод запретить вход
2066
2177
  lobbyModel.publisher.on('join', async ({ hostId, gameId }) => {
2067
- if (gameId && gamesById.has(gameId) && !(await selectActiveGame(gameId))) {
2178
+ if (gameId && gamesById.has(gameId)) {
2179
+ if (!(await selectActiveGame(gameId))) {
2180
+ return;
2181
+ }
2182
+ } else if (!activeGameManifest) {
2183
+ // комната пережила свою игру: её сняли с раздачи, пока комната шла, и
2184
+ // каталог вкладки пуст — заходить не на чем
2185
+ showLobbyError('This room runs a game that is no longer published');
2186
+
2068
2187
  return;
2069
2188
  }
2070
2189
 
@@ -2114,10 +2233,53 @@ function initLobby() {
2114
2233
  () => ({ descriptors: roomFormDescriptors, fields: roomFormFields }),
2115
2234
  );
2116
2235
 
2236
+ // реестр игр (master-game-registry, этап 4): заявка разработчика и панель
2237
+ // модерации живут в том же лобби. Триплет поднимается вместе с лобби —
2238
+ // панель доступна любому авторизованному, кнопку модерации показывает роль
2239
+ const gamesModel = new GamesModel(lobbyConfig.games, () => lobbyAuthModel.getToken());
2240
+ const gamesView = new GamesView(gamesModel, lobbyConfig.games);
2241
+ const games = new GamesCtrl(gamesModel, gamesView);
2242
+
2243
+ games.setAdmin(ADMIN_ROLES.includes(lobbyAuthModel.getRole()));
2244
+
2245
+ // «Test»: манифест застейдженной версии кладётся в каталог вкладки, и
2246
+ // админ поднимает по нему комнату обычной кнопкой Create server
2247
+ games.publisher.on('staged', ({ manifest }) => {
2248
+ registerGameManifest(manifest);
2249
+ gamesView.hide();
2250
+
2251
+ // каталог был пуст (первое развёртывание платформы либо снятая с раздачи
2252
+ // последняя игра): черновик — единственная игра вкладки, и создание
2253
+ // комнаты включается только после того, как её плагин привязан
2254
+ if (activeGameManifest) {
2255
+ return;
2256
+ }
2257
+
2258
+ // строго по очереди: loadStaged() восстанавливает все черновики этого
2259
+ // мастера синхронным циклом, и без очереди два черновика при пустом
2260
+ // каталоге ушли бы активироваться одновременно — активной осталась бы
2261
+ // одна игра, а форма комнаты собралась бы по другой
2262
+ stagedActivation = stagedActivation.then(async () => {
2263
+ if (activeGameManifest) {
2264
+ return; // предыдущий черновик уже стал активным
2265
+ }
2266
+
2267
+ // только на успехе: отказ активации уже написал в ту же строку СВОЮ
2268
+ // причину («Failed to load X: …»), и общее «игр пока не опубликовано»
2269
+ // затёрло бы её — админ увидел бы не то, что случилось
2270
+ if (await selectActiveGame(manifest.id)) {
2271
+ syncCatalogState();
2272
+ }
2273
+ }).catch(e => {
2274
+ // отказ не должен заклинить очередь: следующий черновик обязан
2275
+ // получить свой шанс стать активной игрой
2276
+ console.error('[game] staged activation failed:', e);
2277
+ });
2278
+ });
2279
+
2117
2280
  // создание комнаты в этой же вкладке (хост-игрок через loopback)
2118
2281
  populateGameSelect();
2119
- populateRoomForm(activeGameManifest);
2120
- lobby.gameChanged(activeGameManifest.id, activeGameManifest.title);
2282
+ syncCatalogState();
2121
2283
 
2122
2284
  // селектор игр: меняет форму создания комнаты и Leaderboard сразу
2123
2285
  // (синхронно, без сети); сама игра активируется уже по клику
@@ -1053,3 +1053,104 @@ body.hide-cursor {
1053
1053
  color: #f56;
1054
1054
  margin: 8px 0;
1055
1055
  }
1056
+
1057
+ /*games*/
1058
+
1059
+ /* панель реестра игр (master-game-registry, этап 4): открывается вместо
1060
+ #lobby теми же кнопками бейджа пользователя. Каркас — тот же, что у
1061
+ лобби (.card, .form-row, .lobby-column), собственных цветовых переменных
1062
+ секция не заводит */
1063
+ #games-panel {
1064
+ background-color: #222;
1065
+ position: absolute;
1066
+ width: 100%;
1067
+ min-height: 100%;
1068
+ top: 0;
1069
+ left: 0;
1070
+ z-index: 8;
1071
+ display: flex;
1072
+ align-items: flex-start;
1073
+ justify-content: center;
1074
+ box-sizing: border-box;
1075
+ }
1076
+
1077
+ #games-mine,
1078
+ #games-moderation {
1079
+ width: 700px;
1080
+ padding: 10px;
1081
+ text-align: left;
1082
+ }
1083
+
1084
+ /* заголовок панели и кнопка возврата в одной строке */
1085
+ .games-head {
1086
+ display: flex;
1087
+ align-items: center;
1088
+ justify-content: space-between;
1089
+ }
1090
+
1091
+ .games-list {
1092
+ list-style: none;
1093
+ margin: 0;
1094
+ padding: 0;
1095
+ }
1096
+
1097
+ .games-item {
1098
+ border-bottom: 1px solid #444;
1099
+ padding: 8px;
1100
+ }
1101
+
1102
+ .games-item-title {
1103
+ font-weight: bold;
1104
+ }
1105
+
1106
+ /* поля прямо в карточке заявки: версия и причина отклонения относятся к
1107
+ конкретной строке списка, а не к форме панели */
1108
+ .games-version-input,
1109
+ .games-note-input {
1110
+ width: 200px;
1111
+ text-align: left;
1112
+ margin-right: 8px;
1113
+ }
1114
+
1115
+ /* поля заявки — текстовые, а не счётчики: правило .form-row
1116
+ input[type='text'] рассчитано на «max players» room-формы */
1117
+ #games-submit-form .form-row input[type='text'] {
1118
+ width: 260px;
1119
+ max-width: 100%;
1120
+ text-align: left;
1121
+ }
1122
+
1123
+ .games-filters {
1124
+ display: flex;
1125
+ gap: 12px;
1126
+ border-bottom: 1px solid #444;
1127
+ margin-bottom: 12px;
1128
+ }
1129
+
1130
+ .games-filter-btn {
1131
+ background: none;
1132
+ border: none;
1133
+ color: #888;
1134
+ font-weight: bold;
1135
+ padding: 6px 4px 8px;
1136
+ margin: 0;
1137
+ text-shadow: none;
1138
+ }
1139
+
1140
+ .games-filter-btn.active {
1141
+ color: #eee;
1142
+ }
1143
+
1144
+ /* кнопки входа в панель стоят рядом с Sign out в бейдже пользователя */
1145
+ .lobby-user-actions {
1146
+ display: flex;
1147
+ gap: 8px;
1148
+ align-items: center;
1149
+ }
1150
+
1151
+ @media (max-width: 900px) {
1152
+ #games-mine,
1153
+ #games-moderation {
1154
+ width: 100%;
1155
+ }
1156
+ }
@@ -135,6 +135,37 @@ export function ensureCanvas(canvasId, size, container = document.body) {
135
135
  return canvas;
136
136
  }
137
137
 
138
+ /**
139
+ * Терминальный отказ загрузки — поверх страницы, а не вместо неё.
140
+ *
141
+ * Раньше здесь стояло `document.body.textContent = …`: разметка страницы
142
+ * стиралась целиком, вместе с формой входа и панелью реестра игр, — то есть
143
+ * любая ошибка загрузки игры уносила с собой и путь к её исправлению.
144
+ * `#tech-informer` для того и есть: полноэкранный слой поверх (z-index 9),
145
+ * под которым разметка остаётся целой и рабочей после перезагрузки вкладки.
146
+ *
147
+ * Слой ТЕРМИНАЛЬНЫЙ и снять его нечем — он кроет и `#lobby-auth`, и
148
+ * `#games-panel` (обе z-index 8). Поэтому сюда попадает только то, что и
149
+ * правда терминально: отсутствие каталога само по себе не терминально, и в
150
+ * lobby-режиме оно идёт строкой отказа лобби (client/lib/catalogState.js).
151
+ * @param {string} text - Сообщение игроку.
152
+ * @param {HTMLElement} [container] - Точка монтирования каркаса.
153
+ * @returns {void}
154
+ */
155
+ export function showBootFailure(text, container = document.body) {
156
+ let elem = document.getElementById('tech-informer');
157
+
158
+ if (!elem) {
159
+ // каркаса нет (отказ случился до ensureGameShell): добавляем узел ПЕРЕД
160
+ // содержимым, а не вместо него
161
+ elem = createNode({ tag: 'div', id: 'tech-informer' });
162
+ container.prepend(elem);
163
+ }
164
+
165
+ elem.textContent = text;
166
+ elem.style.display = 'block';
167
+ }
168
+
138
169
  /**
139
170
  * Набор id, которые собирает каркас. Нужен тесту паритета с pug.
140
171
  * @returns {Set<string>}
@@ -0,0 +1,36 @@
1
+ //- Реестр игр в лобби (master-game-registry, этап 4): разработчик подаёт
2
+ //- заявку и следит за её статусом, админ модерирует и играет в черновик.
3
+ //- Панель скрыта по умолчанию — открывается кнопками бейджа пользователя
4
+ //- (#lobby-user в lobby.pug); id элементов перечислены в config/lobby.js.
5
+ //- Подписи — по-английски, как и остальное лобби
6
+ div#games-panel(style='display:none')
7
+ div.lobby-column
8
+ div.card#games-mine
9
+ div.games-head
10
+ h3.lobby-section-title= 'My games'
11
+ input#games-close(type='button', value='Back to lobby')
12
+ ul#games-mine-list.games-list
13
+ form#games-submit-form
14
+ div.form-row
15
+ label.form-label(for='games-field-id')= 'Game id'
16
+ input#games-field-id.field-text(type='text', name='id', maxlength='32', autocomplete='off')
17
+ div.form-row
18
+ label.form-label(for='games-field-package')= 'npm package'
19
+ input#games-field-package.field-text(type='text', name='packageName', maxlength='214', autocomplete='off')
20
+ div.form-row
21
+ label.form-label(for='games-field-version')= 'Version'
22
+ input#games-field-version.field-text(type='text', name='version', maxlength='64', autocomplete='off')
23
+ div.form-row
24
+ label.form-label(for='games-field-repo')= 'Repository URL'
25
+ input#games-field-repo.field-text(type='text', name='repoUrl', maxlength='256', autocomplete='off')
26
+ div.form-row
27
+ label.form-label(for='games-field-title')= 'Title'
28
+ input#games-field-title.field-text(type='text', name='title', maxlength='64', autocomplete='off')
29
+ div#games-submit-error.form-error
30
+ input#games-submit(type='submit', value='Submit for review')
31
+
32
+ div.card#games-moderation(style='display:none')
33
+ h3.lobby-section-title= 'Moderation'
34
+ div#games-filters.games-filters
35
+ ul#games-admin-list.games-list
36
+ div#games-admin-error.form-error
@@ -14,7 +14,13 @@ div#lobby(style='display:none')
14
14
  div
15
15
  span= 'Signed in as '
16
16
  b#lobby-user-nick
17
- input#lobby-user-logout(type='button', value='Sign out')
17
+ //- вход в панель реестра игр (master-game-registry, этап 4):
18
+ //- «My games» видит любой авторизованный, «Moderation» — только
19
+ //- админская роль (main.js по LobbyAuthModel.getRole())
20
+ div.lobby-user-actions
21
+ input#games-open-mine(type='button', value='My games')
22
+ input#games-open-moderation(type='button', value='Moderation', style='display:none')
23
+ input#lobby-user-logout(type='button', value='Sign out')
18
24
  hr.lobby-divider
19
25
  h3.lobby-section-title= 'HOST A GAME'
20
26
  div#lobby-create
@@ -4,5 +4,6 @@ include includes/chat
4
4
  include includes/stat
5
5
  include includes/lobbyAuth
6
6
  include includes/lobby
7
+ include includes/games
7
8
  include includes/auth
8
9
  include includes/informer
package/src/config/env.js CHANGED
@@ -26,10 +26,10 @@ export function applyMasterEnv(config, env = process.env) {
26
26
  config.set('master:security:authServiceUrl', env.VIMP_AUTH_SERVICE_URL);
27
27
  }
28
28
 
29
- // список игр-плагинов мастера (Этап A2), по образцу остальных *_MATRIX
30
- // env-переопределений JSON-массив {id, package}
31
- if (env.GAMES_MATRIX) {
32
- config.set('master:games', JSON.parse(env.GAMES_MATRIX));
29
+ // корень хранилища игровых пакетов (направление master-game-registry)
30
+ // в проде это смонтированный том, переживающий пересоздание контейнера
31
+ if (env.VIMP_GAMES_DIR) {
32
+ config.set('master:gameStore:dir', env.VIMP_GAMES_DIR);
33
33
  }
34
34
  }
35
35
 
@@ -11,10 +11,19 @@ export default {
11
11
 
12
12
  // каталог карт мастера, per-game (Этап 6.4): комната хоста стартует на
13
13
  // актуальных картах активной игры, недоступность каталога — fallback на
14
- // карты из бандла
14
+ // карты из бандла.
15
+ //
16
+ // Аргумент — МАНИФЕСТ, а не gameId (master-game-registry, этап 3): карты
17
+ // клиент берёт не из assetsBase, а по отдельному URL, и версионность
18
+ // каталога обязана доехать и сюда, иначе комната на застейдженной версии
19
+ // играла бы на картах одобренной. `mapsBase` проставляет мастер при
20
+ // ребейзе версионного каталога; его отсутствие (dev, standalone,
21
+ // dedicated, старый мастер) — законный случай, тогда работает прежний
22
+ // путь по id
15
23
  maps: {
16
- manifestUrl: gameId => `/games/${gameId}/maps/manifest.json`,
17
- baseUrl: gameId => `/games/${gameId}/maps`,
24
+ manifestUrl: manifest =>
25
+ `${manifest.mapsBase ?? `/games/${manifest.id}/maps`}/manifest.json`,
26
+ baseUrl: manifest => manifest.mapsBase ?? `/games/${manifest.id}/maps`,
18
27
  },
19
28
 
20
29
  // манифест конкретной игры (Этап 6.5): эстафета Worker'ов перечитывает его
@@ -184,12 +193,88 @@ export default {
184
193
  linkId: 'lobby-link',
185
194
  },
186
195
 
196
+ // реестр игр (master-game-registry, этап 4): заявка разработчика и панель
197
+ // модерации живут в том же лобби, без правки конфигов и рестартов.
198
+ // Все URL и id элементов — здесь: правило репозитория, модули их не
199
+ // хардкодят (games.pug)
200
+ games: {
201
+ urls: {
202
+ // заявки вызывающего со статусами и замечаниями модератора
203
+ mine: '/games/mine',
204
+ // заявка на новую игру платформы (валидируется мастером до записи)
205
+ submit: '/games/submit',
206
+ // заявка на новую версию уже заведённой игры
207
+ version: id => `/games/mine/${encodeURIComponent(id)}/version`,
208
+ // очередь модерации целиком плюс локальное состояние на этом мастере
209
+ admin: '/admin/games',
210
+ // манифесты застейдженных версий — по ним админ поднимает тестовую
211
+ // комнату, не трогая каталог игроков
212
+ staged: '/admin/games/manifest.json',
213
+ // «Test»: скачать версию и положить её в каталог не раздаваемой
214
+ stage: id => `/admin/games/${encodeURIComponent(id)}/stage`,
215
+ // решение модератора
216
+ moderate: id => `/admin/games/${encodeURIComponent(id)}`,
217
+ // что опубликовано в npm — индикатор «есть версия новее»
218
+ versions: id => `/admin/games/${encodeURIComponent(id)}/versions`,
219
+ },
220
+
221
+ // фильтры очереди модерации: id статуса реестра -> подпись кнопки.
222
+ // Значения обязаны совпадать со статусами auth-сервиса
223
+ statuses: [
224
+ { id: 'pending', title: 'Pending' },
225
+ { id: 'approved', title: 'Published' },
226
+ { id: 'rejected', title: 'Rejected' },
227
+ { id: 'disabled', title: 'Disabled' },
228
+ ],
229
+ defaultStatus: 'pending',
230
+
231
+ // суффикс игры, поднятой из застейдженной версии: в селекторе она
232
+ // стоит рядом с одобренной, и различать их обязано быть видно
233
+ stagedSuffix: ' (test)',
234
+
235
+ // DOM-элементы панели (из games.pug)
236
+ elems: {
237
+ panelId: 'games-panel',
238
+ // панель и лобби делят место: открытая панель прячет #lobby целиком
239
+ lobbyId: 'lobby',
240
+ // кнопки в бейдже пользователя (lobby.pug)
241
+ openMineBtnId: 'games-open-mine',
242
+ openModerationBtnId: 'games-open-moderation',
243
+ closeBtnId: 'games-close',
244
+
245
+ // «My games»
246
+ mineListId: 'games-mine-list',
247
+ submitFormId: 'games-submit-form',
248
+ submitErrorId: 'games-submit-error',
249
+ submitBtnId: 'games-submit',
250
+ fieldIds: {
251
+ id: 'games-field-id',
252
+ packageName: 'games-field-package',
253
+ version: 'games-field-version',
254
+ repoUrl: 'games-field-repo',
255
+ title: 'games-field-title',
256
+ },
257
+
258
+ // модерация
259
+ moderationId: 'games-moderation',
260
+ adminListId: 'games-admin-list',
261
+ adminErrorId: 'games-admin-error',
262
+ filtersId: 'games-filters',
263
+ },
264
+ },
265
+
187
266
  // создание комнаты (хост в этой же вкладке); лимит игроков/время
188
267
  // раунда-карты/огонь по своим/карта по умолчанию — из roomDefaults
189
268
  // манифеста активной игры (Этап 6.3), не бандлятся здесь
190
269
  create: {
191
270
  defaultName: 'My Server',
192
271
 
272
+ // каталог платформы пуст: реестр ещё ничего не одобрил либо модератор
273
+ // снял с раздачи последнюю игру. Комнату создавать не на чем, но лобби
274
+ // живо — и текст называет то единственное, что выводит его из этого
275
+ // состояния
276
+ emptyCatalogText: 'No games are published yet — see “My games”',
277
+
193
278
  // период heartbeat/актуализации комнаты у мастера (мс); должен быть
194
279
  // меньше master.host.heartbeatTimeout (30 c), иначе комнату выметет
195
280
  heartbeatInterval: 10000,