vimp-engine 0.24.1 → 0.25.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 +2 -1
- package/src/client/components/controller/Games.js +99 -0
- package/src/client/components/model/Games.js +209 -0
- package/src/client/components/model/LobbyAuth.js +8 -0
- package/src/client/components/view/Games.js +341 -0
- package/src/client/lib/gameActivator.js +13 -5
- package/src/client/main.js +78 -9
- package/src/client/style.css +101 -0
- package/src/client/views/includes/games.pug +36 -0
- package/src/client/views/includes/lobby.pug +7 -1
- package/src/client/views/index.pug +1 -0
- package/src/config/env.js +6 -0
- package/src/config/lobby.js +82 -3
- package/src/config/master.js +33 -13
- package/src/devtools/contract/rules/b2-engine-api.js +21 -6
- package/src/devtools/contract/rules/c10-auth-schema.js +17 -2
- package/src/lib/loadGamePackage.js +39 -20
- package/src/lib/validators.js +10 -9
|
@@ -8,8 +8,14 @@
|
|
|
8
8
|
// Вынесено из main.js (бутстрап, тестами не покрывается) отдельным модулем,
|
|
9
9
|
// чтобы кеш и обработка отказа проверялись юнит-тестом.
|
|
10
10
|
export function createGameActivator({ gamesById, loadClientPlugin }) {
|
|
11
|
-
// gameId -> промис загрузки: кешируется именно
|
|
12
|
-
// клика подряд запустили бы импорт
|
|
11
|
+
// `${gameId}@${manifest.version}` -> промис загрузки: кешируется именно
|
|
12
|
+
// промис, иначе два быстрых клика подряд запустили бы импорт дважды.
|
|
13
|
+
//
|
|
14
|
+
// Версия в ключе обязательна (master-game-registry, этап 3): в каталоге
|
|
15
|
+
// мастера две версии одной игры живут одновременно (админ стейджит новую,
|
|
16
|
+
// игроки играют в одобренную), и ключ по одному gameId вернул бы уже
|
|
17
|
+
// выполненный import() НЕ ТОГО кода. manifest.version — хеш бандла, то
|
|
18
|
+
// есть идентификатор самого кода
|
|
13
19
|
const plugins = new Map();
|
|
14
20
|
|
|
15
21
|
return async function activateGame(gameId) {
|
|
@@ -19,18 +25,20 @@ export function createGameActivator({ gamesById, loadClientPlugin }) {
|
|
|
19
25
|
throw new Error(`unknown game "${gameId}"`);
|
|
20
26
|
}
|
|
21
27
|
|
|
22
|
-
|
|
28
|
+
const key = `${gameId}@${manifest.version}`;
|
|
29
|
+
|
|
30
|
+
let pending = plugins.get(key);
|
|
23
31
|
|
|
24
32
|
if (!pending) {
|
|
25
33
|
pending = loadClientPlugin(manifest).catch(e => {
|
|
26
34
|
// отказ не кешируем: сеть могла моргнуть, повторный клик обязан
|
|
27
35
|
// попробовать снова, а не переигрывать ту же ошибку вечно
|
|
28
|
-
plugins.delete(
|
|
36
|
+
plugins.delete(key);
|
|
29
37
|
|
|
30
38
|
throw e;
|
|
31
39
|
});
|
|
32
40
|
|
|
33
|
-
plugins.set(
|
|
41
|
+
plugins.set(key, pending);
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
return { manifest, plugin: await pending };
|
package/src/client/main.js
CHANGED
|
@@ -73,6 +73,9 @@ import LobbyCtrl from './components/controller/Lobby.js';
|
|
|
73
73
|
import LobbyAuthModel from './components/model/LobbyAuth.js';
|
|
74
74
|
import LobbyAuthView from './components/view/LobbyAuth.js';
|
|
75
75
|
import LobbyAuthCtrl from './components/controller/LobbyAuth.js';
|
|
76
|
+
import GamesModel from './components/model/Games.js';
|
|
77
|
+
import GamesView from './components/view/Games.js';
|
|
78
|
+
import GamesCtrl from './components/controller/Games.js';
|
|
76
79
|
import BakingProvider from './providers/BakingProvider.js';
|
|
77
80
|
import DependencyProvider from './providers/DependencyProvider.js';
|
|
78
81
|
import { HOT_FLAGS } from '../config/opcodes.js';
|
|
@@ -1718,8 +1721,7 @@ let hostRegistration = null;
|
|
|
1718
1721
|
// Этап 5.1/6.4: скачивает каталог карт мастера активной игры (манифест +
|
|
1719
1722
|
// все карты)
|
|
1720
1723
|
async function fetchMasterMaps() {
|
|
1721
|
-
const
|
|
1722
|
-
const manifestRes = await fetch(lobbyConfig.maps.manifestUrl(gameId));
|
|
1724
|
+
const manifestRes = await fetch(lobbyConfig.maps.manifestUrl(activeGameManifest));
|
|
1723
1725
|
|
|
1724
1726
|
if (!manifestRes.ok) {
|
|
1725
1727
|
throw new Error(`maps manifest: HTTP ${manifestRes.status}`);
|
|
@@ -1729,7 +1731,7 @@ async function fetchMasterMaps() {
|
|
|
1729
1731
|
|
|
1730
1732
|
const entries = await Promise.all(
|
|
1731
1733
|
manifest.maps.map(async name => {
|
|
1732
|
-
const url = `${lobbyConfig.maps.baseUrl(
|
|
1734
|
+
const url = `${lobbyConfig.maps.baseUrl(activeGameManifest)}/${encodeURIComponent(name)}`;
|
|
1733
1735
|
const res = await fetch(url);
|
|
1734
1736
|
|
|
1735
1737
|
if (!res.ok) {
|
|
@@ -1840,8 +1842,16 @@ async function fetchServers({ offset, limit, search }) {
|
|
|
1840
1842
|
params.set('limit', limit);
|
|
1841
1843
|
}
|
|
1842
1844
|
|
|
1845
|
+
// токен едет и сюда (master-game-registry, этап 4): список серверов
|
|
1846
|
+
// публичный, но админу мастер отдаёт вместе с ним скрытые тестовые
|
|
1847
|
+
// комнаты застейдженных версий
|
|
1848
|
+
const token = lobbyAuthModel?.getToken();
|
|
1849
|
+
|
|
1843
1850
|
try {
|
|
1844
|
-
const res = await fetch(
|
|
1851
|
+
const res = await fetch(
|
|
1852
|
+
`${lobbyConfig.serversUrl}?${params}`,
|
|
1853
|
+
token ? { headers: { authorization: `Bearer ${token}` } } : undefined,
|
|
1854
|
+
);
|
|
1845
1855
|
|
|
1846
1856
|
return res.ok ? await res.json() : null;
|
|
1847
1857
|
} catch (e) {
|
|
@@ -1939,6 +1949,11 @@ function populateRoomForm(manifest) {
|
|
|
1939
1949
|
}
|
|
1940
1950
|
}
|
|
1941
1951
|
|
|
1952
|
+
// роли, которым лобби показывает кнопку «Moderation» (master-game-registry,
|
|
1953
|
+
// этап 4). Это подсказка интерфейсу: доступ к данным проверяет мастер, а
|
|
1954
|
+
// запись — auth-сервис, перечитывая роль из БД
|
|
1955
|
+
const ADMIN_ROLES = ['admin', 'superadmin'];
|
|
1956
|
+
|
|
1942
1957
|
// каталог манифестов по id: форма и leaderboard селектора игр, а также
|
|
1943
1958
|
// активация игры перед созданием комнаты и входом в чужую
|
|
1944
1959
|
const gamesById = new Map(
|
|
@@ -1972,7 +1987,10 @@ function clearLobbyError() {
|
|
|
1972
1987
|
// активирует игру и перепривязывает манифест/плагин/CSS; false — отказ уже
|
|
1973
1988
|
// показан игроку, лобби остаётся рабочим
|
|
1974
1989
|
async function selectActiveGame(gameId) {
|
|
1975
|
-
|
|
1990
|
+
// сверка идёт с самим манифестом, а не с его id: каталог вкладки может
|
|
1991
|
+
// пополниться застейдженной версией той же игры (registerGameManifest), и
|
|
1992
|
+
// сравнение по id оставило бы активной одобренную версию
|
|
1993
|
+
if (gameId === activeGameManifest.id && gamesById.get(gameId) === activeGameManifest) {
|
|
1976
1994
|
return true;
|
|
1977
1995
|
}
|
|
1978
1996
|
|
|
@@ -1993,8 +2011,36 @@ async function selectActiveGame(gameId) {
|
|
|
1993
2011
|
}
|
|
1994
2012
|
}
|
|
1995
2013
|
|
|
2014
|
+
// id игр, чей манифест в каталоге вкладки подменён застейдженной версией
|
|
2015
|
+
// (master-game-registry, этап 4): в селекторе такая игра помечается, чтобы
|
|
2016
|
+
// админ видел, что поднимет комнату НЕ на одобренной версии
|
|
2017
|
+
const stagedGameIds = new Set();
|
|
2018
|
+
|
|
2019
|
+
/**
|
|
2020
|
+
* Кладёт манифест застейдженной (не одобренной) версии в каталог вкладки —
|
|
2021
|
+
* так админ может поднять по нему комнату, не трогая каталог игроков.
|
|
2022
|
+
* Дальше всё работает существующим путём: createGameActivator грузит
|
|
2023
|
+
* ClientPlugin по entries.client (ключ кеша версионный, поэтому плагин
|
|
2024
|
+
* черновика не подменит одобренный), connectAsHost поднимает комнату, а
|
|
2025
|
+
* мастер помечает её скрытой по версии манифеста.
|
|
2026
|
+
* @param {Object} manifest - Манифест застейдженной версии из /admin/games.
|
|
2027
|
+
* @returns {void}
|
|
2028
|
+
*/
|
|
2029
|
+
function registerGameManifest(manifest) {
|
|
2030
|
+
if (!manifest?.id) {
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
gamesById.set(manifest.id, manifest);
|
|
2035
|
+
stagedGameIds.add(manifest.id);
|
|
2036
|
+
populateGameSelect();
|
|
2037
|
+
}
|
|
2038
|
+
|
|
1996
2039
|
// заполняет #lobby-game всем каталогом манифестов мастера — раньше туда
|
|
1997
|
-
// попадал только gamesManifest[0], теперь селектор
|
|
2040
|
+
// попадал только gamesManifest[0], теперь селектор рабочий.
|
|
2041
|
+
// Источник — gamesById, а не gamesManifest: каталог вкладки пополняется на
|
|
2042
|
+
// лету застейдженными версиями (registerGameManifest), и селектор
|
|
2043
|
+
// перерисовывается целиком
|
|
1998
2044
|
function populateGameSelect() {
|
|
1999
2045
|
const gameSelect = document.getElementById(lobbyConfig.elems.gameId);
|
|
2000
2046
|
|
|
@@ -2002,7 +2048,13 @@ function populateGameSelect() {
|
|
|
2002
2048
|
return;
|
|
2003
2049
|
}
|
|
2004
2050
|
|
|
2005
|
-
|
|
2051
|
+
// перерисовка не должна сбрасывать выбор игрока: селектор целиком
|
|
2052
|
+
// пересобирается и при добавлении застейдженной версии
|
|
2053
|
+
const selected = gameSelect.value;
|
|
2054
|
+
|
|
2055
|
+
gameSelect.textContent = '';
|
|
2056
|
+
|
|
2057
|
+
gamesById.forEach(manifest => {
|
|
2006
2058
|
const option = document.createElement('option');
|
|
2007
2059
|
const available = isGameAvailable(manifest);
|
|
2008
2060
|
|
|
@@ -2011,14 +2063,15 @@ function populateGameSelect() {
|
|
|
2011
2063
|
// каталога, и игрок видел пустое лобби без причины), но выбрать её
|
|
2012
2064
|
// нельзя: комнату по ней всё равно не поднять
|
|
2013
2065
|
option.textContent = available
|
|
2014
|
-
? manifest.title
|
|
2066
|
+
? `${manifest.title}${stagedGameIds.has(manifest.id) ? lobbyConfig.games.stagedSuffix : ''}`
|
|
2015
2067
|
: `${manifest.title} — unavailable`;
|
|
2016
2068
|
option.disabled = !available;
|
|
2017
2069
|
option.title = available ? '' : manifest.compat.text;
|
|
2018
2070
|
gameSelect.appendChild(option);
|
|
2019
2071
|
});
|
|
2020
2072
|
|
|
2021
|
-
gameSelect.value =
|
|
2073
|
+
gameSelect.value =
|
|
2074
|
+
selected && gamesById.has(selected) ? selected : activeGameManifest.id;
|
|
2022
2075
|
}
|
|
2023
2076
|
|
|
2024
2077
|
// поднимает лобби после welcome от мастера (iceServers уже получены);
|
|
@@ -2114,6 +2167,22 @@ function initLobby() {
|
|
|
2114
2167
|
() => ({ descriptors: roomFormDescriptors, fields: roomFormFields }),
|
|
2115
2168
|
);
|
|
2116
2169
|
|
|
2170
|
+
// реестр игр (master-game-registry, этап 4): заявка разработчика и панель
|
|
2171
|
+
// модерации живут в том же лобби. Триплет поднимается вместе с лобби —
|
|
2172
|
+
// панель доступна любому авторизованному, кнопку модерации показывает роль
|
|
2173
|
+
const gamesModel = new GamesModel(lobbyConfig.games, () => lobbyAuthModel.getToken());
|
|
2174
|
+
const gamesView = new GamesView(gamesModel, lobbyConfig.games);
|
|
2175
|
+
const games = new GamesCtrl(gamesModel, gamesView);
|
|
2176
|
+
|
|
2177
|
+
games.setAdmin(ADMIN_ROLES.includes(lobbyAuthModel.getRole()));
|
|
2178
|
+
|
|
2179
|
+
// «Test»: манифест застейдженной версии кладётся в каталог вкладки, и
|
|
2180
|
+
// админ поднимает по нему комнату обычной кнопкой Create server
|
|
2181
|
+
games.publisher.on('staged', ({ manifest }) => {
|
|
2182
|
+
registerGameManifest(manifest);
|
|
2183
|
+
gamesView.hide();
|
|
2184
|
+
});
|
|
2185
|
+
|
|
2117
2186
|
// создание комнаты в этой же вкладке (хост-игрок через loopback)
|
|
2118
2187
|
populateGameSelect();
|
|
2119
2188
|
populateRoomForm(activeGameManifest);
|
package/src/client/style.css
CHANGED
|
@@ -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
|
+
}
|
|
@@ -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
|
-
|
|
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
|
package/src/config/env.js
CHANGED
|
@@ -31,6 +31,12 @@ export function applyMasterEnv(config, env = process.env) {
|
|
|
31
31
|
if (env.GAMES_MATRIX) {
|
|
32
32
|
config.set('master:games', JSON.parse(env.GAMES_MATRIX));
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
// корень хранилища игровых пакетов (направление master-game-registry) —
|
|
36
|
+
// в проде это смонтированный том, переживающий пересоздание контейнера
|
|
37
|
+
if (env.VIMP_GAMES_DIR) {
|
|
38
|
+
config.set('master:gameStore:dir', env.VIMP_GAMES_DIR);
|
|
39
|
+
}
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
/**
|
package/src/config/lobby.js
CHANGED
|
@@ -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:
|
|
17
|
-
|
|
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,6 +193,76 @@ 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), не бандлятся здесь
|
package/src/config/master.js
CHANGED
|
@@ -18,22 +18,42 @@ export default {
|
|
|
18
18
|
cert: path.join(rootDir, '.certs', 'cert.pem'),
|
|
19
19
|
},
|
|
20
20
|
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
21
|
+
// Статический каталог игр-плагинов. ПУСТ по умолчанию
|
|
22
|
+
// (master-game-registry, этап 3): штатный источник каталога — реестр игр
|
|
23
|
+
// auth-сервиса, откуда мастер скачивает одобренные пакеты сам
|
|
24
|
+
// (master/GameSync.js). Движок игр не знает и зависимостями их не тянет.
|
|
25
|
+
//
|
|
26
|
+
// Массив и переменная окружения GAMES_MATRIX (JSON) остаются
|
|
27
|
+
// переопределением для двух случаев: локальная разработка (каталог вдобавок
|
|
28
|
+
// дополняется собранными пакетами @vimp-games/* из node_modules,
|
|
29
|
+
// master/localGames.js — прилинкованная игра важнее реестра) и self-hosted
|
|
30
|
+
// мастер без реестра. `package` — имя npm-пакета игры, резолвится через
|
|
31
|
+
// node_modules; версию задаёт сама установленная зависимость.
|
|
32
32
|
// maxGameScore (snakes-v3 этап 3.3, необязательное поле рядом с
|
|
33
33
|
// id/package) — потолок результата ОДНОЙ игры для этой игры; мастер
|
|
34
|
-
// клампит им `best`/`points` PUT /auth/rank.
|
|
34
|
+
// клампит им `best`/`points` PUT /auth/rank. Для игр реестра его задаёт
|
|
35
|
+
// админ при модерации, здесь — только запасной путь. Не задан — дефолт
|
|
35
36
|
// master:playerData:maxGameScore
|
|
36
|
-
games: [
|
|
37
|
+
games: [],
|
|
38
|
+
|
|
39
|
+
// Хранилище игровых пакетов (направление master-game-registry): мастер
|
|
40
|
+
// качает одобренные игры из npm registry и раздаёт их с диска, вместо того
|
|
41
|
+
// чтобы получать их npm-зависимостью на этапе сборки образа.
|
|
42
|
+
gameStore: {
|
|
43
|
+
// корень хранилища; null → <repoRoot>/.games (локальная разработка).
|
|
44
|
+
// В проде задаётся VIMP_GAMES_DIR и монтируется томом
|
|
45
|
+
dir: null,
|
|
46
|
+
registryUrl: 'https://registry.npmjs.org',
|
|
47
|
+
// период опроса реестра auth за изменениями каталога
|
|
48
|
+
refreshInterval: 60000,
|
|
49
|
+
// потолки распаковки недоверенного архива
|
|
50
|
+
maxTarballBytes: 64 * 1024 * 1024,
|
|
51
|
+
maxFiles: 5000,
|
|
52
|
+
// сколько версий одной игры держать на диске: активная + стейджевая
|
|
53
|
+
keepVersions: 2,
|
|
54
|
+
// потолок ожидания ответа реестра
|
|
55
|
+
timeout: 30000,
|
|
56
|
+
},
|
|
37
57
|
|
|
38
58
|
// список серверов (GET /servers)
|
|
39
59
|
servers: {
|
|
@@ -123,7 +123,7 @@ export default {
|
|
|
123
123
|
// исключена: пакет, собранный до появления поля, обязан оставаться
|
|
124
124
|
// валидным (И1/И2)
|
|
125
125
|
function halfViolations(ctx, manifestRequires) {
|
|
126
|
-
const wanted = new Set(manifestRequires);
|
|
126
|
+
const wanted = new Set(resolveNames(manifestRequires));
|
|
127
127
|
const violations = [];
|
|
128
128
|
const halves = [
|
|
129
129
|
['HostPlugin', ctx.hostPlugin?.requires],
|
|
@@ -139,7 +139,7 @@ function halfViolations(ctx, manifestRequires) {
|
|
|
139
139
|
continue;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
const extra = value.filter(name => !wanted.has(name));
|
|
142
|
+
const extra = resolveNames(value).filter(name => !wanted.has(name));
|
|
143
143
|
|
|
144
144
|
if (extra.length) {
|
|
145
145
|
violations.push(
|
|
@@ -158,10 +158,12 @@ function halfViolations(ctx, manifestRequires) {
|
|
|
158
158
|
return violations;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
const declared =
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
const declared = resolveNames(
|
|
162
|
+
halves
|
|
163
|
+
.map(([, value]) => value)
|
|
164
|
+
.filter(Array.isArray)
|
|
165
|
+
.flat(),
|
|
166
|
+
);
|
|
165
167
|
|
|
166
168
|
const missing = [...wanted].filter(name => !declared.includes(name));
|
|
167
169
|
|
|
@@ -176,6 +178,19 @@ function halfViolations(ctx, manifestRequires) {
|
|
|
176
178
|
return violations;
|
|
177
179
|
}
|
|
178
180
|
|
|
181
|
+
// Имена сравниваются РЕЗОЛВНУТЫМИ: реестр возможностей append-only, и
|
|
182
|
+
// выведенное алиасом имя движок принимает вечно наравне с активным.
|
|
183
|
+
// Манифест с новым именем и половина, оставшаяся на выведенном, просят одно
|
|
184
|
+
// и то же — считать это расхождением значило бы ругаться на игру за то, что
|
|
185
|
+
// движок переименовал возможность (тот самый отказ по возрасту, который план
|
|
186
|
+
// снимал). Неизвестное имя резолва не имеет и сравнивается как есть: о нём
|
|
187
|
+
// говорит проверка по реестру выше
|
|
188
|
+
function resolveNames(names) {
|
|
189
|
+
return (Array.isArray(names) ? names : []).map(
|
|
190
|
+
name => ENGINE_CAPABILITIES.resolve(name) ?? name,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
179
194
|
// то же, что делают B5 и C10: выведенное имя — не отказ, а предупреждение.
|
|
180
195
|
// Отвергать за него значило бы отвергать игру за возраст (И1)
|
|
181
196
|
function finish(violations, retired) {
|
|
@@ -2,6 +2,7 @@ import { ERROR, WARN, skip, verdict } from '../result.js';
|
|
|
2
2
|
import { resolveValidator } from '../../../lib/validators.js';
|
|
3
3
|
import {
|
|
4
4
|
formControls,
|
|
5
|
+
resolveDescriptor,
|
|
5
6
|
ACTIVE_FORM_CONTROLS,
|
|
6
7
|
} from '../../../lib/formControls.js';
|
|
7
8
|
|
|
@@ -13,6 +14,10 @@ import {
|
|
|
13
14
|
// которого нет в authSchema.validators (поле не проверяется никем).
|
|
14
15
|
const NICKNAME = /^(name|nick|nickname|player|playername|login|username)$/i;
|
|
15
16
|
|
|
17
|
+
// те же имена, что у validateAuth и билдера формы: список вариантов есть
|
|
18
|
+
// только у них, и только им движок резолвит `source`
|
|
19
|
+
const OPTION_CONTROLS = ['select', 'radio'];
|
|
20
|
+
|
|
16
21
|
export default {
|
|
17
22
|
id: 'C10',
|
|
18
23
|
name: 'authSchema',
|
|
@@ -76,8 +81,18 @@ export default {
|
|
|
76
81
|
// значит список вариантов у такого поля пуст всегда: игрок видит
|
|
77
82
|
// 'no options available' и войти не может, а хост отвергает любое
|
|
78
83
|
// значение как не-вариант. Это только для authSchema; в roomForm
|
|
79
|
-
// (правило B5) source штатный — там движок отдаёт каталог
|
|
80
|
-
|
|
84
|
+
// (правило B5) source штатный — там движок отдаёт каталог карт.
|
|
85
|
+
//
|
|
86
|
+
// Только для контролов со списком: у text/checkbox `source` не читает
|
|
87
|
+
// никто (formBuilder.resolveOptions зовут лишь buildSelect/buildRadio),
|
|
88
|
+
// и такое поле работает — отвергать за лишний ключ значило бы отвергать
|
|
89
|
+
// игру, которая логинит игроков как ни в чём не бывало
|
|
90
|
+
// по резолвнутому контролу, а не по написанному: 'segmented' — та же
|
|
91
|
+
// группа вариантов, что radio, и source в ней так же не резолвится
|
|
92
|
+
if (
|
|
93
|
+
OPTION_CONTROLS.includes(resolveDescriptor(field.options)?.control) &&
|
|
94
|
+
field.options?.source !== undefined
|
|
95
|
+
) {
|
|
81
96
|
violations.push(
|
|
82
97
|
`authSchema param "${field.name}" declares options.source ` +
|
|
83
98
|
`"${field.options.source}" — the auth form is built without ` +
|