vimp-engine 0.24.1 → 0.24.2
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
CHANGED
|
@@ -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 ` +
|
|
@@ -2,6 +2,7 @@ import { access, readFile } from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { checkPluginCompatibility } from './gamePlugin.js';
|
|
5
|
+
import { ENGINE_CAPABILITIES } from './capabilities.js';
|
|
5
6
|
|
|
6
7
|
// Загрузка пакета игры в Node по его собранному dist/ (Этап 4 плана
|
|
7
8
|
// standalone-sdk). В браузере плагин грузится по URL из GameManifest мастера;
|
|
@@ -107,25 +108,28 @@ function assertPluginMatchesManifest(manifest, plugins) {
|
|
|
107
108
|
}
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
|
|
111
|
+
warnOnRequiresMismatch(manifest, plugins);
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
// `requires` пишут ТРИ независимых места одного пакета игры: скрипт сборки
|
|
114
115
|
// манифеста и обе половины плагина (последние — ради standalone SDK, у
|
|
115
116
|
// которого манифеста нет вовсе). Разъехавшись, они дают игру, которая в
|
|
116
|
-
// лобби честно отвергается, а в solo-режиме тихо
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
117
|
+
// лобби честно отвергается, а в solo-режиме тихо недоигрывает.
|
|
118
|
+
//
|
|
119
|
+
// Здесь это ПРЕДУПРЕЖДЕНИЕ, а не отказ, и разница принципиальная. Node-путь
|
|
120
|
+
// (dedicated, vimp-sim, inline host) читает авторитетный манифест, и его
|
|
121
|
+
// совместимость уже проверена выше (checkPluginCompatibility): рассинхрон с
|
|
122
|
+
// половинами ничего здесь не ломает. Бросить значило бы отвергнуть пакет,
|
|
123
|
+
// который грузился раньше, — прямое нарушение И4 плана
|
|
124
|
+
// plugin-forward-compat ради дефекта упаковки, который движок переживает.
|
|
125
|
+
// Место отказа — правило контракта B2: чекер запускает автор игры, и он
|
|
126
|
+
// узнаёт о расхождении до публикации, а не игрок вместо матча.
|
|
120
127
|
//
|
|
121
128
|
// Половина, вовсе НЕ объявившая поле, из сверки исключена: старый пакет,
|
|
122
|
-
// собранный до его появления,
|
|
123
|
-
// (`requires: []`) — уже утверждение «игре ничего не
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const wanted = new Set(
|
|
127
|
-
Array.isArray(manifest.requires) ? manifest.requires : [],
|
|
128
|
-
);
|
|
129
|
+
// собранный до его появления, ничего не утверждает (И1/И2). Объявленное
|
|
130
|
+
// пустым (`requires: []`) — уже утверждение «игре ничего не нужно».
|
|
131
|
+
function warnOnRequiresMismatch(manifest, plugins) {
|
|
132
|
+
const wanted = new Set(resolveNames(manifest.requires));
|
|
129
133
|
|
|
130
134
|
for (const [half, plugin] of Object.entries(plugins)) {
|
|
131
135
|
if (plugin.requires === undefined || plugin.requires === null) {
|
|
@@ -133,16 +137,19 @@ function assertRequiresMatchManifest(manifest, plugins) {
|
|
|
133
137
|
}
|
|
134
138
|
|
|
135
139
|
if (!Array.isArray(plugin.requires)) {
|
|
136
|
-
|
|
140
|
+
console.warn(
|
|
137
141
|
`game "${manifest.id}": ${half} plugin requires must be an array ` +
|
|
138
|
-
'of capability names',
|
|
142
|
+
'of capability names — the standalone SDK reads it',
|
|
139
143
|
);
|
|
144
|
+
continue;
|
|
140
145
|
}
|
|
141
146
|
|
|
142
|
-
const extra = plugin.requires.filter(
|
|
147
|
+
const extra = resolveNames(plugin.requires).filter(
|
|
148
|
+
name => !wanted.has(name),
|
|
149
|
+
);
|
|
143
150
|
|
|
144
151
|
if (extra.length) {
|
|
145
|
-
|
|
152
|
+
console.warn(
|
|
146
153
|
`game "${manifest.id}": ${half} plugin requires ` +
|
|
147
154
|
`${extra.join(', ')}, which manifest.requires does not list — ` +
|
|
148
155
|
'stale dist/ (the manifest is what the lobby master reads)',
|
|
@@ -156,19 +163,18 @@ function assertRequiresMatchManifest(manifest, plugins) {
|
|
|
156
163
|
// (`requires: []`) — это утверждение «ничего не нужно», и оно расходится
|
|
157
164
|
// с манифестом наравне с неполным списком
|
|
158
165
|
const halves = Object.values(plugins).filter(
|
|
159
|
-
plugin =>
|
|
166
|
+
plugin => Array.isArray(plugin.requires),
|
|
160
167
|
);
|
|
161
168
|
|
|
162
169
|
if (halves.length === 0) {
|
|
163
170
|
return;
|
|
164
171
|
}
|
|
165
172
|
|
|
166
|
-
const declared = halves.flatMap(plugin => plugin.requires);
|
|
167
|
-
|
|
173
|
+
const declared = resolveNames(halves.flatMap(plugin => plugin.requires));
|
|
168
174
|
const missing = [...wanted].filter(name => !declared.includes(name));
|
|
169
175
|
|
|
170
176
|
if (missing.length) {
|
|
171
|
-
|
|
177
|
+
console.warn(
|
|
172
178
|
`game "${manifest.id}": manifest.requires names ${missing.join(', ')}, ` +
|
|
173
179
|
'which neither plugin half declares — stale dist/ (the standalone ' +
|
|
174
180
|
'SDK reads the halves, there is no manifest in solo mode)',
|
|
@@ -176,6 +182,19 @@ function assertRequiresMatchManifest(manifest, plugins) {
|
|
|
176
182
|
}
|
|
177
183
|
}
|
|
178
184
|
|
|
185
|
+
// Сравнивать имена возможностей буквально нельзя: реестр append-only, и
|
|
186
|
+
// выведенное алиасом имя движок принимает вечно наравне с активным
|
|
187
|
+
// (ENGINE_CAPABILITIES.has в checkPluginCompatibility). Манифест, назвавший
|
|
188
|
+
// новое имя, и половина, оставшаяся на выведенном, просят ОДНО И ТО ЖЕ —
|
|
189
|
+
// объявить это расхождением значило бы ругаться на игру за то, что движок
|
|
190
|
+
// переименовал возможность. Неизвестное имя резолва не имеет и сравнивается
|
|
191
|
+
// как есть: о нём говорит checkPluginCompatibility, а не эта сверка
|
|
192
|
+
function resolveNames(names) {
|
|
193
|
+
return (Array.isArray(names) ? names : []).map(
|
|
194
|
+
name => ENGINE_CAPABILITIES.resolve(name) ?? name,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
179
198
|
function importDefault(baseDir, entry, assetsBase) {
|
|
180
199
|
const file = path.resolve(baseDir, stripBase(entry, assetsBase));
|
|
181
200
|
|
package/src/lib/validators.js
CHANGED
|
@@ -56,15 +56,16 @@ const numericError = (options, value) => {
|
|
|
56
56
|
return `must be <= ${options.max}`;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
// regExp
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
// regExp числового поля хост НЕ проверяет, и это осознанно. Клиент
|
|
60
|
+
// сверяет паттерн с сырой строкой, которую набрал игрок
|
|
61
|
+
// (formBuilder.validateField: field.getRaw()); до хоста доезжает уже
|
|
62
|
+
// число, и восстановить ту строку нечем — `String(значение)` даёт
|
|
63
|
+
// каноническую запись, а не набранную. Паттерн, принимающий
|
|
64
|
+
// неканоническую («^\d+\.\d{2}$» и ввод «1.50»), совпал бы с формой и
|
|
65
|
+
// разошёлся бы с хостом: игрок видит поле валидным, а вход не пускает, и
|
|
66
|
+
// набрать строку, годную обеим сторонам, нельзя вовсе. Блокировка входа
|
|
67
|
+
// хуже той щели, которую паттерн закрывает: диапазон уже проверен, а
|
|
68
|
+
// ведущий ноль до хоста и не доезжает (Number('08') === 8)
|
|
68
69
|
return null;
|
|
69
70
|
};
|
|
70
71
|
|