vimp-engine 0.25.0 → 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.
- package/package.json +1 -1
- package/src/client/components/model/Games.js +6 -0
- package/src/client/components/view/Games.js +28 -4
- package/src/client/lib/catalogState.js +44 -0
- package/src/client/main.js +108 -15
- package/src/client/views/gameShell.js +31 -0
- package/src/config/env.js +0 -6
- package/src/config/lobby.js +6 -0
- package/src/config/master.js +5 -5
package/package.json
CHANGED
|
@@ -162,6 +162,12 @@ export default class GamesModel {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
await this.loadAdmin();
|
|
165
|
+
|
|
166
|
+
// после loadAdmin, а не до: renderAdmin чистит строку отказа панели, и
|
|
167
|
+
// предупреждение, показанное раньше, стёрлось бы тем же тиком
|
|
168
|
+
if (json?.warning) {
|
|
169
|
+
this.publisher.emit('warning', { scope: 'admin', code: json.warning });
|
|
170
|
+
}
|
|
165
171
|
}
|
|
166
172
|
|
|
167
173
|
_emitAdmin() {
|
|
@@ -28,6 +28,13 @@ const ERROR_MESSAGES = {
|
|
|
28
28
|
authServiceUnavailable: 'Registry service unavailable',
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
// предупреждения: решение принято, отказа не было, но состояние платформы
|
|
32
|
+
// стоит назвать вслух
|
|
33
|
+
const WARNING_MESSAGES = {
|
|
34
|
+
catalogEmpty:
|
|
35
|
+
'No published games left — the lobby cannot create rooms until one is published',
|
|
36
|
+
};
|
|
37
|
+
|
|
31
38
|
const STATUS_TITLES = {
|
|
32
39
|
pending: 'in review',
|
|
33
40
|
approved: 'published',
|
|
@@ -71,10 +78,20 @@ export default class GamesView {
|
|
|
71
78
|
this._filters = document.getElementById(elems.filtersId);
|
|
72
79
|
|
|
73
80
|
this._fields = new Map(
|
|
74
|
-
Object.entries(elems.fieldIds).map(([name, id]) =>
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
Object.entries(elems.fieldIds).map(([name, id]) => {
|
|
82
|
+
const field = document.getElementById(id);
|
|
83
|
+
|
|
84
|
+
// карта строится по конфигу, а используется из обработчиков событий:
|
|
85
|
+
// разъехавшийся с games.pug id дал бы здесь null, а упал бы позже —
|
|
86
|
+
// безымянным TypeError внутри clearForm или _readForm. Остальная
|
|
87
|
+
// разметка панели проверяется тем же способом, только неявно: первое
|
|
88
|
+
// же обращение к отсутствующему элементу бросает в конструкторе
|
|
89
|
+
if (!field) {
|
|
90
|
+
throw new Error(`GamesView: no element "#${id}" for field "${name}"`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return [name, field];
|
|
94
|
+
}),
|
|
78
95
|
);
|
|
79
96
|
|
|
80
97
|
this.publisher = new Publisher();
|
|
@@ -96,6 +113,7 @@ export default class GamesView {
|
|
|
96
113
|
mp.on('mine-changed', 'renderMine', this);
|
|
97
114
|
mp.on('admin-changed', 'renderAdmin', this);
|
|
98
115
|
mp.on('error', 'renderError', this);
|
|
116
|
+
mp.on('warning', 'renderWarning', this);
|
|
99
117
|
}
|
|
100
118
|
|
|
101
119
|
// кнопку модерации показывает не сама панель, а роль вызывающего
|
|
@@ -180,6 +198,12 @@ export default class GamesView {
|
|
|
180
198
|
});
|
|
181
199
|
}
|
|
182
200
|
|
|
201
|
+
renderWarning({ scope, code }) {
|
|
202
|
+
const container = scope === 'admin' ? this._adminError : this._submitError;
|
|
203
|
+
|
|
204
|
+
container.textContent = WARNING_MESSAGES[code] ?? code;
|
|
205
|
+
}
|
|
206
|
+
|
|
183
207
|
renderError({ scope, errors }) {
|
|
184
208
|
const container = scope === 'admin' ? this._adminError : this._submitError;
|
|
185
209
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Каталог игр платформы может быть ПУСТ, и это законное состояние лобби, а не
|
|
2
|
+
// отказ загрузки: игры живут в реестре auth-сервиса, модератор вправе снять с
|
|
3
|
+
// раздачи последнюю, а на первом развёртывании не одобрена ещё ни одна.
|
|
4
|
+
//
|
|
5
|
+
// Лобби в этом состоянии обязано жить целиком: вход, бейдж пользователя, «My
|
|
6
|
+
// games» и «Moderation» от игры не зависят — и только через них каталог
|
|
7
|
+
// возвращается к жизни. Раньше пустой каталог бросал из бутстрапа и стирал
|
|
8
|
+
// разметку страницы: модератор, отключивший последнюю игру, запирал сам себя
|
|
9
|
+
// и вернуть её мог только запросом мимо интерфейса.
|
|
10
|
+
//
|
|
11
|
+
// Вынесено из main.js (бутстрап, в happy-dom не поднимается) отдельным
|
|
12
|
+
// модулем, чтобы обе ветки проверялись юнит-тестом.
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Приводит лобби в соответствие с активной игрой.
|
|
16
|
+
* @param {Object} [manifest] - Манифест активной игры; `undefined` — каталог
|
|
17
|
+
* платформы пуст.
|
|
18
|
+
* @param {Object} deps - Проводка лобби.
|
|
19
|
+
* @param {HTMLElement} [deps.hostBtn] - Кнопка «Create server».
|
|
20
|
+
* @param {string} deps.emptyText - Строка отказа для пустого каталога.
|
|
21
|
+
* @param {Function} deps.bindGame - Форма комнаты и Leaderboard активной игры.
|
|
22
|
+
* @param {Function} deps.showError - Показать строку отказа лобби.
|
|
23
|
+
* @param {Function} deps.clearError - Снять строку отказа лобби.
|
|
24
|
+
* @returns {void}
|
|
25
|
+
*/
|
|
26
|
+
export function applyCatalogState(
|
|
27
|
+
manifest,
|
|
28
|
+
{ hostBtn, emptyText, bindGame, showError, clearError },
|
|
29
|
+
) {
|
|
30
|
+
if (manifest) {
|
|
31
|
+
bindGame(manifest);
|
|
32
|
+
clearError();
|
|
33
|
+
} else {
|
|
34
|
+
// форму комнаты и Leaderboard не трогаем вовсе: без игры у первой нет
|
|
35
|
+
// схемы полей, а второй — игры, за рейтингом которой идти
|
|
36
|
+
showError(emptyText);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (hostBtn) {
|
|
40
|
+
hostBtn.disabled = !manifest;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default applyCatalogState;
|
package/src/client/main.js
CHANGED
|
@@ -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 {
|
|
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';
|
|
@@ -102,6 +107,10 @@ let activeGameManifest;
|
|
|
102
107
|
let clientPlugin;
|
|
103
108
|
let gamesManifest;
|
|
104
109
|
|
|
110
|
+
// почему активной игры нет, если каталог непуст: строка отказа лобби вместо
|
|
111
|
+
// общего «игр пока не опубликовано» (см. pickActiveGame)
|
|
112
|
+
let catalogProblem = null;
|
|
113
|
+
|
|
105
114
|
// узел игрового CSS: при переключении игры его текст заменяется, иначе стили
|
|
106
115
|
// двух игр жили бы в head одновременно и конфликтовали селекторами
|
|
107
116
|
let gameStyleNode = null;
|
|
@@ -157,20 +166,40 @@ try {
|
|
|
157
166
|
throw gamesManifest;
|
|
158
167
|
}
|
|
159
168
|
|
|
160
|
-
|
|
161
|
-
|
|
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
|
+
}
|
|
162
185
|
}
|
|
163
186
|
|
|
164
|
-
|
|
187
|
+
// пустой каталог — состояние лобби, а не отказ загрузки: модератор вправе
|
|
188
|
+
// отключить последнюю игру, реестр вправе ещё ничего не одобрить, и вернуть
|
|
189
|
+
// каталог к жизни можно только из панели, которая живёт в этой же вкладке.
|
|
190
|
+
// В solo/dedicated игры нет — это отказ: там весь смысл вкладки в матче
|
|
191
|
+
if (!activeGameManifest && !isLobbyMode) {
|
|
165
192
|
throw new Error('master has no games in its catalog');
|
|
166
193
|
}
|
|
167
194
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
195
|
+
if (activeGameManifest) {
|
|
196
|
+
bindActiveGame(
|
|
197
|
+
activeGameManifest,
|
|
198
|
+
boot.clientPlugin ?? (await loadClientPlugin(activeGameManifest)),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
172
201
|
} catch (e) {
|
|
173
|
-
|
|
202
|
+
showBootFailure(`Failed to load the game: ${e.message}`, gameContainer);
|
|
174
203
|
throw e;
|
|
175
204
|
}
|
|
176
205
|
|
|
@@ -1990,7 +2019,12 @@ async function selectActiveGame(gameId) {
|
|
|
1990
2019
|
// сверка идёт с самим манифестом, а не с его id: каталог вкладки может
|
|
1991
2020
|
// пополниться застейдженной версией той же игры (registerGameManifest), и
|
|
1992
2021
|
// сравнение по id оставило бы активной одобренную версию
|
|
1993
|
-
|
|
2022
|
+
// activeGameManifest?. — каталог платформы может быть пуст: активной игры
|
|
2023
|
+
// у вкладки тогда нет вовсе, и первая же активация обязана состояться
|
|
2024
|
+
if (
|
|
2025
|
+
gameId === activeGameManifest?.id &&
|
|
2026
|
+
gamesById.get(gameId) === activeGameManifest
|
|
2027
|
+
) {
|
|
1994
2028
|
return true;
|
|
1995
2029
|
}
|
|
1996
2030
|
|
|
@@ -2011,6 +2045,9 @@ async function selectActiveGame(gameId) {
|
|
|
2011
2045
|
}
|
|
2012
2046
|
}
|
|
2013
2047
|
|
|
2048
|
+
// очередь активации черновиков (см. подписку на 'staged' в initLobby)
|
|
2049
|
+
let stagedActivation = Promise.resolve();
|
|
2050
|
+
|
|
2014
2051
|
// id игр, чей манифест в каталоге вкладки подменён застейдженной версией
|
|
2015
2052
|
// (master-game-registry, этап 4): в селекторе такая игра помечается, чтобы
|
|
2016
2053
|
// админ видел, что поднимет комнату НЕ на одобренной версии
|
|
@@ -2070,8 +2107,29 @@ function populateGameSelect() {
|
|
|
2070
2107
|
gameSelect.appendChild(option);
|
|
2071
2108
|
});
|
|
2072
2109
|
|
|
2073
|
-
|
|
2074
|
-
|
|
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
|
+
});
|
|
2075
2133
|
}
|
|
2076
2134
|
|
|
2077
2135
|
// поднимает лобби после welcome от мастера (iceServers уже получены);
|
|
@@ -2117,7 +2175,15 @@ function initLobby() {
|
|
|
2117
2175
|
// хостов старше 6.4 — тогда заходим на активной игре, как раньше: неизвестная
|
|
2118
2176
|
// версия не повод запретить вход
|
|
2119
2177
|
lobbyModel.publisher.on('join', async ({ hostId, gameId }) => {
|
|
2120
|
-
if (gameId && gamesById.has(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
|
+
|
|
2121
2187
|
return;
|
|
2122
2188
|
}
|
|
2123
2189
|
|
|
@@ -2181,12 +2247,39 @@ function initLobby() {
|
|
|
2181
2247
|
games.publisher.on('staged', ({ manifest }) => {
|
|
2182
2248
|
registerGameManifest(manifest);
|
|
2183
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
|
+
});
|
|
2184
2278
|
});
|
|
2185
2279
|
|
|
2186
2280
|
// создание комнаты в этой же вкладке (хост-игрок через loopback)
|
|
2187
2281
|
populateGameSelect();
|
|
2188
|
-
|
|
2189
|
-
lobby.gameChanged(activeGameManifest.id, activeGameManifest.title);
|
|
2282
|
+
syncCatalogState();
|
|
2190
2283
|
|
|
2191
2284
|
// селектор игр: меняет форму создания комнаты и Leaderboard сразу
|
|
2192
2285
|
// (синхронно, без сети); сама игра активируется уже по клику
|
|
@@ -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>}
|
package/src/config/env.js
CHANGED
|
@@ -26,12 +26,6 @@ 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));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
29
|
// корень хранилища игровых пакетов (направление master-game-registry) —
|
|
36
30
|
// в проде это смонтированный том, переживающий пересоздание контейнера
|
|
37
31
|
if (env.VIMP_GAMES_DIR) {
|
package/src/config/lobby.js
CHANGED
|
@@ -269,6 +269,12 @@ export default {
|
|
|
269
269
|
create: {
|
|
270
270
|
defaultName: 'My Server',
|
|
271
271
|
|
|
272
|
+
// каталог платформы пуст: реестр ещё ничего не одобрил либо модератор
|
|
273
|
+
// снял с раздачи последнюю игру. Комнату создавать не на чем, но лобби
|
|
274
|
+
// живо — и текст называет то единственное, что выводит его из этого
|
|
275
|
+
// состояния
|
|
276
|
+
emptyCatalogText: 'No games are published yet — see “My games”',
|
|
277
|
+
|
|
272
278
|
// период heartbeat/актуализации комнаты у мастера (мс); должен быть
|
|
273
279
|
// меньше master.host.heartbeatTimeout (30 c), иначе комнату выметет
|
|
274
280
|
heartbeatInterval: 10000,
|
package/src/config/master.js
CHANGED
|
@@ -23,11 +23,11 @@ export default {
|
|
|
23
23
|
// auth-сервиса, откуда мастер скачивает одобренные пакеты сам
|
|
24
24
|
// (master/GameSync.js). Движок игр не знает и зависимостями их не тянет.
|
|
25
25
|
//
|
|
26
|
-
// Массив и
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
26
|
+
// Массив остаётся двум потребителям, и env-переопределения у него нет:
|
|
27
|
+
// локальной разработке (каталог наполняют собранные пакеты @vimp-games/*
|
|
28
|
+
// из node_modules, master/localGames.js — прилинкованная игра важнее
|
|
29
|
+
// реестра) и dedicated-серверу, который берёт отсюда игру, названную
|
|
30
|
+
// VIMP_DEDICATED_GAME. `package` — имя npm-пакета игры, резолвится через
|
|
31
31
|
// node_modules; версию задаёт сама установленная зависимость.
|
|
32
32
|
// maxGameScore (snakes-v3 этап 3.3, необязательное поле рядом с
|
|
33
33
|
// id/package) — потолок результата ОДНОЙ игры для этой игры; мастер
|