vimp-engine 0.38.2 → 0.38.3
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/core/Cargo.toml +1 -1
- package/package.json +3 -1
- package/src/client/boot.js +30 -7
- package/src/client/components/controller/ClientReports.js +1 -1
- package/src/client/components/controller/Games.js +1 -1
- package/src/client/components/model/ClientReports.js +1 -1
- package/src/client/components/model/Games.js +24 -0
- package/src/client/components/view/ClientReports.js +1 -1
- package/src/client/components/view/Games.js +19 -1
- package/src/client/components/view/Lobby.js +1 -7
- package/src/client/debug.js +1 -1
- package/src/client/lib/authApi.js +1 -1
- package/src/client/lib/diagnostics.js +19 -4
- package/src/client/lib/engineVersion.js +0 -6
- package/src/client/lib/footerLink.js +15 -14
- package/src/client/lib/retryOnNetworkError.js +60 -0
- package/src/client/main.js +35 -18
- package/src/client/network/HiddenHostHealthLog.js +40 -3
- package/src/client/network/HostController.js +4 -4
- package/src/client/network/HostHealthPolicy.js +41 -0
- package/src/client/network/InlineHostBridge.js +8 -1
- package/src/client/session/HandoffFlow.js +5 -0
- package/src/client/session/HostRole.js +5 -2
- package/src/client/style.css +5 -6
- package/src/client/views/gameShell.js +3 -3
- package/src/client/views/includes/auth.pug +4 -4
- package/src/client/views/includes/lobby.pug +4 -4
- package/src/client/views/includes/reports.pug +1 -1
- package/src/config/authClient.js +3 -3
- package/src/config/lobby.js +4 -5
- package/src/config/opcodes.js +2 -1
- package/src/devtools/contract/loadContext.js +2 -2
- package/src/devtools/contract/rules/a7-package-repository.js +7 -8
- package/src/devtools/contract/rules/index.js +1 -1
- package/src/devtools/invariants.js +1 -1
- package/src/host/DebugRecorder.js +1 -1
- package/src/host/HostGame.js +35 -15
- package/src/host/commandFailure.js +140 -0
- package/src/host/host.worker.js +8 -2
- package/src/host/meta/SocketManager.js +2 -2
- package/src/host/meta/core/CommandProcessor.js +37 -7
- package/src/lib/applyRoomOverrides.js +1 -1
- package/src/lib/capabilities.js +1 -1
- package/src/lib/checkpointCodec.js +1 -2
- package/src/lib/coreConfig.js +1 -1
- package/src/lib/createHostRuntime.js +1 -1
- package/src/lib/gameId.js +1 -1
- package/src/lib/gamePlugin.js +6 -2
- package/src/lib/jwt.js +1 -1
- package/src/lib/packageLink.js +18 -61
- package/src/lib/validators.js +3 -3
- package/src/standalone/index.js +1 -1
- package/tests/fixtures/generations/gen-api3/README.md +2 -2
- package/tests/fixtures/generations/gen-api4/README.md +2 -2
- package/src/devtools/surface/abiParse.js +0 -175
- package/src/devtools/surface/collect.js +0 -413
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Отчёт о сбое чат-команды игры для приёмника диагностики
|
|
2
|
+
// (HostGame.setDiagnosticSink): сообщение в одну строку и стек без текста
|
|
3
|
+
// ошибки. Один отчёт на все среды — Worker (журнал платформы), dedicated
|
|
4
|
+
// (лог процесса), standalone (консоль вкладки). Чистые функции, без DOM и
|
|
5
|
+
// Node API: модуль работает в Worker'е
|
|
6
|
+
|
|
7
|
+
// символы, подделывающие строки лога: управляющие (перевод строки, ESC),
|
|
8
|
+
// невидимые форматные (bidi-переопределения, нулевая ширина, BOM), разделители
|
|
9
|
+
// строк и абзацев (U+2028/2029)
|
|
10
|
+
const LOG_UNSAFE = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\u180E]/gu;
|
|
11
|
+
const MAX_STACK = 2000;
|
|
12
|
+
|
|
13
|
+
// обрезка до limit единиц UTF-16 без разрыва суррогатной пары; выполняется
|
|
14
|
+
// до очистки, чтобы огромная строка не проходила regex целиком
|
|
15
|
+
const clip = (value, limit) => {
|
|
16
|
+
if (value.length <= limit) {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const text = value.slice(0, limit);
|
|
21
|
+
|
|
22
|
+
return /[\uD800-\uDBFF]$/.test(text) ? text.slice(0, -1) : text;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// одна строка лога: текст от игрока или из кода игры без LOG_UNSAFE
|
|
26
|
+
const oneLine = (value, limit) =>
|
|
27
|
+
clip(String(value), limit).replace(LOG_UNSAFE, ' ');
|
|
28
|
+
|
|
29
|
+
// стек сохраняет переводы строк, остальное LOG_UNSAFE заменяется; длина
|
|
30
|
+
// ограничена
|
|
31
|
+
const cleanStack = stack =>
|
|
32
|
+
clip(stack, MAX_STACK)
|
|
33
|
+
.split(/\r\n|\r|\n/)
|
|
34
|
+
.map(line => line.replace(LOG_UNSAFE, ' '))
|
|
35
|
+
.join('\n');
|
|
36
|
+
|
|
37
|
+
// кадры стека (V8: « at …»). Заголовок «имя: текст» снимается по числу
|
|
38
|
+
// строк текста — ввод игрока в тексте не отличить от кадра по виду, поиск
|
|
39
|
+
// «at» его бы не отсёк. Не сошлось (текст изменён после создания, цепочка
|
|
40
|
+
// cause, другой движок) — остаётся хвост из подряд идущих кадров; ни кадров
|
|
41
|
+
// V8, ни кадров SpiderMonkey/JSC — стек сводится к одной строке, чтобы текст
|
|
42
|
+
// из него не подделал строки лога
|
|
43
|
+
const V8_FRAME = /^ {4}at /;
|
|
44
|
+
|
|
45
|
+
// кадр SpiderMonkey/JavaScriptCore: «имя@место», текста ошибки в таком стеке
|
|
46
|
+
// нет. Имя — без пробелов, кроме служебных имён JSC (global/eval/module code,
|
|
47
|
+
// «async имя», у анонимной функции — «async »): заголовок V8 «имя: текст» за
|
|
48
|
+
// кадр не сойдёт. Место — URL со схемой, [native code] или кадр Wasm у JSC:
|
|
49
|
+
// wasm-function[N] с именем модуля или без, у WebKit до 2025-08 — [wasm code]
|
|
50
|
+
// (имя тогда «модуль.wasm-function[N]»). Голый [native code] — тоже кадр
|
|
51
|
+
const FRAME_NAME = [
|
|
52
|
+
String.raw`[^\s@]*`,
|
|
53
|
+
'(?:global|eval|module) code',
|
|
54
|
+
String.raw`async [^\s@]*`,
|
|
55
|
+
].join('|');
|
|
56
|
+
const FRAME_PLACE = [
|
|
57
|
+
String.raw`[a-z][a-z0-9+.-]*:\S+`,
|
|
58
|
+
String.raw`\[native code\]`,
|
|
59
|
+
String.raw`\[wasm code\]`,
|
|
60
|
+
String.raw`(?:[^\s@:[\]]+:)?wasm-function\[\d+\]`,
|
|
61
|
+
].join('|');
|
|
62
|
+
const AT_FRAME = new RegExp(
|
|
63
|
+
String.raw`^(?:(?:${FRAME_NAME})@(?:${FRAME_PLACE})|\[native code\])$`,
|
|
64
|
+
'i',
|
|
65
|
+
);
|
|
66
|
+
const STACK_SCAN = 10 * MAX_STACK;
|
|
67
|
+
|
|
68
|
+
const stackFrames = (stack, message) => {
|
|
69
|
+
const lines = clip(stack, STACK_SCAN).split('\n');
|
|
70
|
+
|
|
71
|
+
// обрезка по STACK_SCAN рвёт последнюю строку: неполный кадр не
|
|
72
|
+
// распознала бы ни одна ветка, и весь стек свернулся бы в строку
|
|
73
|
+
if (stack.length > STACK_SCAN && lines.length > 1) {
|
|
74
|
+
lines.pop();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// SpiderMonkey/JavaScriptCore: стек — одни кадры, без заголовка; Firefox
|
|
78
|
+
// заканчивает его переводом строки
|
|
79
|
+
let end = lines.length;
|
|
80
|
+
|
|
81
|
+
while (end > 0 && lines[end - 1] === '') {
|
|
82
|
+
end -= 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const body = lines.slice(0, end);
|
|
86
|
+
|
|
87
|
+
if (body.length > 0 && body.every(line => AT_FRAME.test(line))) {
|
|
88
|
+
return body.join('\n');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const rest = lines.slice(message.split('\n').length);
|
|
92
|
+
|
|
93
|
+
if (rest.length > 0 && rest.every(line => V8_FRAME.test(line))) {
|
|
94
|
+
return rest.join('\n');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let start = lines.length;
|
|
98
|
+
|
|
99
|
+
while (start > 0 && V8_FRAME.test(lines[start - 1])) {
|
|
100
|
+
start -= 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return start < lines.length
|
|
104
|
+
? lines.slice(start).join('\n')
|
|
105
|
+
: lines.join(' | ');
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// сбой чат-команды → { kind, message, stack }. Не бросает: недоступный
|
|
109
|
+
// message/toString — текст «[unprintable error]», недоступный стек — null.
|
|
110
|
+
// Имя команды приходит из ввода игрока, текст — из кода игры, который вправе
|
|
111
|
+
// вставить в него аргументы игрока: оба приводятся к одной строке и
|
|
112
|
+
// обрезаются. V8 повторяет текст ошибки первой строкой стека — она
|
|
113
|
+
// снимается, остаются кадры
|
|
114
|
+
export function commandFailureReport(command, error) {
|
|
115
|
+
let text;
|
|
116
|
+
let stack = null;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
// message читается один раз: геттер мог бы вернуть разное
|
|
120
|
+
const message = error && error.message ? String(error.message) : '';
|
|
121
|
+
|
|
122
|
+
text = message || String(error);
|
|
123
|
+
|
|
124
|
+
if (error && typeof error.stack === 'string') {
|
|
125
|
+
stack = stackFrames(error.stack, message);
|
|
126
|
+
stack = cleanStack(stack) || null;
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
text ??= '[unprintable error]';
|
|
130
|
+
stack = null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const commandText = oneLine(command, 32);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
kind: 'error',
|
|
137
|
+
message: `chat command ${commandText} failed: ${oneLine(text, 400)}`,
|
|
138
|
+
stack,
|
|
139
|
+
};
|
|
140
|
+
}
|
package/src/host/host.worker.js
CHANGED
|
@@ -152,6 +152,12 @@ async function onInit(room, handoff = null, checkpointBytes = null, seqFloor) {
|
|
|
152
152
|
// ли отдать роль
|
|
153
153
|
host.setHealthSink(health => self.postMessage({ type: 'health', health }));
|
|
154
154
|
|
|
155
|
+
// сбой кода игры, пойманный хостом (чат-команда): в журнал клиентских
|
|
156
|
+
// ошибок тем же сообщением, что и необработанный reject
|
|
157
|
+
host.setDiagnosticSink(report =>
|
|
158
|
+
self.postMessage({ type: 'diagnostic', ...report }),
|
|
159
|
+
);
|
|
160
|
+
|
|
155
161
|
// в лобби личность игрока — claim identity-токена, проверенного по JWKS
|
|
156
162
|
// мастера (Этап B3); свободного ввода имени в форме игры нет
|
|
157
163
|
portMachine = new PortMachine({
|
|
@@ -238,7 +244,7 @@ function onDebug({ requestId, action }) {
|
|
|
238
244
|
self.postMessage({ type: 'debug_result', requestId, result, error });
|
|
239
245
|
}
|
|
240
246
|
|
|
241
|
-
// журнал клиентских
|
|
247
|
+
// журнал клиентских ошибок: необработанный reject в
|
|
242
248
|
// Worker не всплывает в worker.onerror главного потока — пересылаем сами.
|
|
243
249
|
// Отдельный тип сообщения: 'error' занят сбоем init и запускает откат эстафеты
|
|
244
250
|
self.addEventListener('unhandledrejection', event => {
|
|
@@ -325,7 +331,7 @@ self.onmessage = async event => {
|
|
|
325
331
|
host?.setRoom({ roomId: msg.hostId, roomSecret: msg.hostSecret });
|
|
326
332
|
break;
|
|
327
333
|
|
|
328
|
-
// отладочный
|
|
334
|
+
// отладочный контур: единственный вход в
|
|
329
335
|
// авторитетную половину из главного потока — запрос/ответ по requestId
|
|
330
336
|
case 'debug':
|
|
331
337
|
onDebug(msg);
|
|
@@ -311,8 +311,8 @@ export default class SocketManager {
|
|
|
311
311
|
}
|
|
312
312
|
|
|
313
313
|
/**
|
|
314
|
-
* Отправка отладочного лога хоста в консоль клиента (порт CONSOLE
|
|
315
|
-
*
|
|
314
|
+
* Отправка отладочного лога хоста в консоль клиента (порт CONSOLE).
|
|
315
|
+
* Worker изолирован от DevTools вкладки —
|
|
316
316
|
* без этого канала его события в браузере не видны вовсе.
|
|
317
317
|
* @param {string} socketId
|
|
318
318
|
* @param {*} data
|
|
@@ -4,11 +4,19 @@
|
|
|
4
4
|
// разных играх может делать разное или отсутствовать вовсе; незнакомая
|
|
5
5
|
// команда — COMMANDS_NOT_FOUND.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// Контекст команд: roundManager (новый раунд, текущая карта, смена карты),
|
|
8
|
+
// timerManager (остаток времени карты), playerDataSync (ранг), chat,
|
|
9
|
+
// isDevMode и остальные поля deps.
|
|
10
|
+
//
|
|
11
|
+
// Обработчик — код игры и вызывается под защитой: его сбой (исключение или
|
|
12
|
+
// отклонённый промис) уходит в onError, матч продолжается.
|
|
10
13
|
class CommandProcessor {
|
|
11
|
-
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} deps - контекст игровых команд.
|
|
16
|
+
* @param {Object} [options]
|
|
17
|
+
* @param {Function} [options.onError] - (command, error) сбой обработчика.
|
|
18
|
+
*/
|
|
19
|
+
constructor(deps, { onError = null } = {}) {
|
|
12
20
|
this._chat = deps.chat;
|
|
13
21
|
|
|
14
22
|
// контекст игровых команд (participants, chat, scripted, roundManager,
|
|
@@ -17,6 +25,7 @@ class CommandProcessor {
|
|
|
17
25
|
this._ctx = deps;
|
|
18
26
|
|
|
19
27
|
this._commands = new Map();
|
|
28
|
+
this._onError = onError;
|
|
20
29
|
}
|
|
21
30
|
|
|
22
31
|
/**
|
|
@@ -36,10 +45,31 @@ class CommandProcessor {
|
|
|
36
45
|
const cmd = arr.shift();
|
|
37
46
|
const handler = this._commands.get(cmd);
|
|
38
47
|
|
|
39
|
-
if (handler) {
|
|
40
|
-
handler(this._ctx, gameId, arr);
|
|
41
|
-
} else {
|
|
48
|
+
if (!handler) {
|
|
42
49
|
this._chat.pushSystemByUser(gameId, 'COMMANDS_NOT_FOUND');
|
|
50
|
+
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// в Worker'е сбой стал бы необработанным reject'ом, в dedicated-процессе —
|
|
55
|
+
// uncaughtException, то есть падением матча всех игроков
|
|
56
|
+
try {
|
|
57
|
+
const result = handler(this._ctx, gameId, arr);
|
|
58
|
+
|
|
59
|
+
if (result && typeof result.then === 'function') {
|
|
60
|
+
result.then(null, error => this._fail(cmd, error));
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
this._fail(cmd, error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// сбой приёмника не должен породить новый сбой
|
|
68
|
+
_fail(command, error) {
|
|
69
|
+
try {
|
|
70
|
+
this._onError?.(command, error);
|
|
71
|
+
} catch {
|
|
72
|
+
// приёмник молчит о своих сбоях
|
|
43
73
|
}
|
|
44
74
|
}
|
|
45
75
|
}
|
|
@@ -54,7 +54,7 @@ export function applyRoomOverrides(
|
|
|
54
54
|
game.timers.mapTime = clampTime(room.mapTime);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// dev-режим вкладки
|
|
57
|
+
// dev-режим вкладки хоста: включает рекордер
|
|
58
58
|
// и хостовый CONSOLE-лог. Прод-бандл его не выставляет — поведение то же
|
|
59
59
|
if (typeof room.isDevMode === 'boolean') {
|
|
60
60
|
game.isDevMode = room.isDevMode;
|
package/src/lib/capabilities.js
CHANGED
|
@@ -43,7 +43,7 @@ export const ENGINE_CAPABILITIES = createRegistry('engine-capabilities', [
|
|
|
43
43
|
// `SimCtx::map_body_state`) и доступ игры к телам карты по индексу
|
|
44
44
|
// (хэндл, индекс по хэндлу, отключение тела)
|
|
45
45
|
{ value: 'map.bodyState', since: '0.34.0' },
|
|
46
|
-
// сервис пула `diagnostics` (журнал клиентских
|
|
46
|
+
// сервис пула `diagnostics` (журнал клиентских ошибок);
|
|
47
47
|
// опционален — игра не пишет его в `requires`
|
|
48
48
|
{ value: 'diagnostics', since: '0.35.0' },
|
|
49
49
|
// контрольная точка хоста посреди раунда (host-migration этап 5):
|
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Контейнер до сжатия: [u32 metaLen LE][meta JSON utf-8][байты дампа ядра];
|
|
6
6
|
// весь буфер — gzip. Дамп ядра сам по себе JSON (serialize_state), и без
|
|
7
|
-
// сжатия точка tanks весит ~420 КБ, с ним ~21
|
|
8
|
-
// spike-results).
|
|
7
|
+
// сжатия точка tanks весит ~420 КБ, с ним ~21 КБ.
|
|
9
8
|
|
|
10
9
|
import hostDefaults from '../config/hostDefaults.js';
|
|
11
10
|
|
package/src/lib/coreConfig.js
CHANGED
|
@@ -2,7 +2,7 @@ import { SNAPSHOT_FORMAT_VERSION } from '../config/opcodes.js';
|
|
|
2
2
|
import hostDefaults from '../config/hostDefaults.js';
|
|
3
3
|
import wsports from '../config/wsports.js';
|
|
4
4
|
|
|
5
|
-
// Сборка JSON-конфига Rust-ядра (
|
|
5
|
+
// Сборка JSON-конфига Rust-ядра (крейт vimp-engine-core + core/ в репозитории
|
|
6
6
|
// игры, например vimp-tanks): движковая половина
|
|
7
7
|
// (timeStep/mapScale/mapSetId/snapshot/seed) + игровая
|
|
8
8
|
// (models/weapons/playerKeys/panel/friendlyFire) — форма {engine, game} из
|
|
@@ -87,7 +87,7 @@ export async function createHostRuntime(room, options = {}) {
|
|
|
87
87
|
// преемник собирает из них тот же конфиг ядра
|
|
88
88
|
roomSettings: pickRoomSettings(room),
|
|
89
89
|
mapsVersion: room?.mapsVersion ?? null,
|
|
90
|
-
// рекордер
|
|
90
|
+
// рекордер кладёт seed в сценарий — без него
|
|
91
91
|
// записанный матч невоспроизводим
|
|
92
92
|
seed,
|
|
93
93
|
...hostOptions,
|
package/src/lib/gameId.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Формат id игры — изоморфно: его проверяет мастер (реестр, раздача, каталог)
|
|
2
2
|
// и клиентский роутер ссылок (client/lib/roomLink.js) до похода в сеть.
|
|
3
|
-
// Значение обязано совпадать с
|
|
3
|
+
// Значение обязано совпадать с games из конфига auth-сервиса
|
|
4
4
|
// (см. master/gameRefs.js — он реэкспортирует отсюда)
|
|
5
5
|
export const GAME_ID_PATTERN = /^[a-z][a-z0-9-]{1,30}$/;
|
|
6
6
|
|
package/src/lib/gamePlugin.js
CHANGED
|
@@ -7,8 +7,12 @@ import { createGameConfigView } from './gameConfigView.js';
|
|
|
7
7
|
// entries.client из манифеста.
|
|
8
8
|
|
|
9
9
|
// каталог всех игр мастера (GameCatalog, см. Этап 6.2) — массив манифестов
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// doFetch подставляет клиент: загрузка страницы повторяет сбой сети
|
|
11
|
+
export async function fetchGamesManifest(
|
|
12
|
+
url = '/games/manifest.json',
|
|
13
|
+
doFetch = fetch,
|
|
14
|
+
) {
|
|
15
|
+
const res = await doFetch(url);
|
|
12
16
|
|
|
13
17
|
if (!res.ok) {
|
|
14
18
|
throw new Error(`games manifest: HTTP ${res.status}`);
|
package/src/lib/jwt.js
CHANGED
|
@@ -41,7 +41,7 @@ export function decodeJwtPayload(token) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// Авторитетная проверка identity-токена (Этап B3): подпись RS256 по JWKS
|
|
44
|
-
// central auth
|
|
44
|
+
// central auth-сервиса, issuer и срок годности. Работает через
|
|
45
45
|
// Web Crypto API (crypto.subtle) — доступен в браузере, Worker'е хоста и в
|
|
46
46
|
// Node ≥19 глобально, отдельной JWT-библиотеки не требует. Возвращает
|
|
47
47
|
// проверенный payload ({ sub, nick, iss, exp, ... }) или бросает исключение.
|
package/src/lib/packageLink.js
CHANGED
|
@@ -1,63 +1,31 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// Адрес проекта пакета — его `repository`: ссылка ведёт на саму игру, в том
|
|
2
|
+
// числе закрытую. `homepage` не читается. Фолбэка нет: пакет без repository
|
|
3
|
+
// адреса не получает — страница пакета в npm маскировала бы недостающие
|
|
4
|
+
// метаданные. О пропущенном поле предупреждает правило контракта A7.
|
|
3
5
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// в npm давал разнобой («GitHub» у одной игры, «npm» у другой) и молча
|
|
8
|
-
// маскировал недостающие метаданные вместо того, чтобы показать их отсутствие.
|
|
9
|
-
// О пропущенном поле предупреждает правило контракта A7.
|
|
10
|
-
//
|
|
11
|
-
// Хостинг не ограничен github.com: подпись выводится из хоста, поэтому чужой
|
|
12
|
-
// плагин на GitLab получает честную ссылку, а не пустую ячейку.
|
|
13
|
-
//
|
|
14
|
-
// Модуль изоморфный: мастер зовёт resolveProjectUrl для пакета игры
|
|
15
|
-
// (GameCatalog), клиент — для собственного package.json движка
|
|
16
|
-
// (client/lib/engineVersion.js).
|
|
6
|
+
// Хостинг не ограничен github.com. Модуль изоморфный: мастер зовёт
|
|
7
|
+
// resolveProjectUrl для метаданных пакета игры (адрес уходит в реестр игр),
|
|
8
|
+
// devtools — для package.json игры (правило A7).
|
|
17
9
|
|
|
18
10
|
/**
|
|
19
|
-
* Адрес проекта из package.json, приведённый к
|
|
20
|
-
* repository.
|
|
11
|
+
* Адрес проекта из package.json, приведённый к http(s): поле repository.
|
|
21
12
|
* @param {Object} pkg - разобранный package.json
|
|
22
|
-
* @returns {string|null}
|
|
13
|
+
* @returns {string|null} http(s)-URL либо null, если поля нет или это не адрес
|
|
23
14
|
*/
|
|
24
15
|
export function resolveProjectUrl(pkg) {
|
|
25
16
|
if (!pkg || typeof pkg !== 'object') {
|
|
26
17
|
return null;
|
|
27
18
|
}
|
|
28
19
|
|
|
29
|
-
const {
|
|
20
|
+
const { repository } = pkg;
|
|
30
21
|
|
|
31
22
|
// repository бывает и строкой ("github:user/repo"), и объектом {type, url}
|
|
32
|
-
|
|
33
|
-
typeof repository === 'string' ? repository : repository?.url
|
|
34
|
-
|
|
35
|
-
// homepage важнее repository: поле repository обязано называть
|
|
36
|
-
// репозиторий публикации (его сверяет npm Trusted Publishing), и у пакета
|
|
37
|
-
// из закрытого репозитория такая ссылка отдаёт игроку 404
|
|
38
|
-
return normalize(homepage) ?? normalizeRepository(declared);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Ссылка футера по готовому адресу проекта.
|
|
43
|
-
* @param {string|null} [url] - результат resolveProjectUrl
|
|
44
|
-
* @returns {{url: string, label: string}|null} null, если адреса нет
|
|
45
|
-
*/
|
|
46
|
-
export function projectLink(url) {
|
|
47
|
-
// адрес перепроверяется, хотя свои вызовы приходят уже из
|
|
48
|
-
// resolveProjectUrl: функция — публичный экспорт ("./lib/*"), а результат
|
|
49
|
-
// уходит прямо в href якоря (client/lib/footerLink.js)
|
|
50
|
-
const value = typeof url === 'string' ? url.trim() : '';
|
|
51
|
-
|
|
52
|
-
if (!/^https?:\/\//.test(value)) {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return { url: value, label: labelFor(value) };
|
|
23
|
+
return normalizeRepository(
|
|
24
|
+
typeof repository === 'string' ? repository : repository?.url,
|
|
25
|
+
);
|
|
57
26
|
}
|
|
58
27
|
|
|
59
|
-
// шорткат "user/repo" — семантика поля repository (так его читает npm)
|
|
60
|
-
// homepage такая строка означала бы относительный путь, а не адрес на GitHub
|
|
28
|
+
// шорткат "user/repo" — семантика поля repository (так его читает npm)
|
|
61
29
|
function normalizeRepository(raw) {
|
|
62
30
|
const value = typeof raw === 'string' ? raw.trim() : '';
|
|
63
31
|
|
|
@@ -94,6 +62,9 @@ function normalize(raw) {
|
|
|
94
62
|
|
|
95
63
|
url = url.replace(/^git\+/, '');
|
|
96
64
|
|
|
65
|
+
// порт SSH (ssh://host:2222/…) к адресу https не относится
|
|
66
|
+
url = url.replace(/^(ssh:\/\/(?:[^/@]*@)?[^/:@]+):\d+(?=\/|$)/, '$1');
|
|
67
|
+
|
|
97
68
|
// scp-форма git@host:user/repo (в том числе с лишним ssh://, как её
|
|
98
69
|
// нередко пишет npm) — двоеточие здесь разделитель, не порт
|
|
99
70
|
const scp = /^(?:ssh:\/\/)?git@([^:/]+):(.+)$/.exec(url);
|
|
@@ -117,22 +88,8 @@ function normalize(raw) {
|
|
|
117
88
|
// в href он уехал бы в DOM и историю браузера
|
|
118
89
|
.replace(/^(https?:\/\/)[^/@]*@/, '$1')
|
|
119
90
|
.replace(/\.git(?=$|[#?])/, '')
|
|
120
|
-
//
|
|
91
|
+
// якорь #readme в адресе репозитория ссылке не нужен
|
|
121
92
|
.replace(/#readme$/, '')
|
|
122
93
|
.replace(/\/+$/, '')
|
|
123
94
|
);
|
|
124
95
|
}
|
|
125
|
-
|
|
126
|
-
// подпись ячейки: у наших пакетов это всегда GitHub, у чужого хостинга —
|
|
127
|
-
// его хост, чтобы подпись не врала об источнике
|
|
128
|
-
function labelFor(url) {
|
|
129
|
-
let host;
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
host = new URL(url).hostname.replace(/^www\./, '');
|
|
133
|
-
} catch {
|
|
134
|
-
return 'Website';
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return host === 'github.com' ? 'GitHub' : host;
|
|
138
|
-
}
|
package/src/lib/validators.js
CHANGED
|
@@ -13,7 +13,7 @@ const NAME_REGEXP = new RegExp('^[a-zA-Z]([\\w\\s#]{0,13})[\\w]{1}$');
|
|
|
13
13
|
export const isValidName = name =>
|
|
14
14
|
typeof name === 'string' && NAME_REGEXP.test(name);
|
|
15
15
|
|
|
16
|
-
// ник профиля: копия правила auth-сервиса (
|
|
16
|
+
// ник профиля: копия правила auth-сервиса (валидаторы auth-сервиса)
|
|
17
17
|
// — общей рантайм-зависимости у пакетов нет. Строже isValidName: внутри ника
|
|
18
18
|
// допустим только обычный пробел, не любой \s
|
|
19
19
|
const NICK_REGEXP = new RegExp('^[a-zA-Z]([\\w #]{0,13})[\\w]{1}$');
|
|
@@ -272,8 +272,8 @@ export const clampLimit = (value, fallback, max) => {
|
|
|
272
272
|
// на его стороне — минута (lobbyConfig.playerData.minFlushInterval), столько
|
|
273
273
|
// игр в неё не влезает.
|
|
274
274
|
//
|
|
275
|
-
// Множитель СВЯЗАН с rank.maxPoints auth
|
|
276
|
-
//
|
|
275
|
+
// Множитель СВЯЗАН с rank.maxPoints из конфига auth-сервиса: мастер
|
|
276
|
+
// не должен пропускать то, что auth отклонит. Отклонённое
|
|
277
277
|
// тело для хоста неустранимо — PlayerDataSync повторял бы его до конца жизни
|
|
278
278
|
// комнаты, — поэтому при правке per-game maxGameScore проверяется и потолок
|
|
279
279
|
// суммы на той стороне.
|
package/src/standalone/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { ensureGameShell } from '../client/views/gameShell.js';
|
|
|
10
10
|
// репозитория игры — `npm run dev` открывает вкладку с работающим матчем без
|
|
11
11
|
// мастера, без входа и без экрана лобби.
|
|
12
12
|
//
|
|
13
|
-
// Отличия от прода (
|
|
13
|
+
// Отличия от прода (осознанные):
|
|
14
14
|
// - хост крутится в главном потоке (inline), а не в Worker'е: HostPlugin
|
|
15
15
|
// непередаваем постмесседжем, а SDK получает живой объект плагина;
|
|
16
16
|
// - личность гостевая (ник из формы или из playerName), рейтинг и
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# gen-api3 — ЗАМОРОЖЕНО
|
|
2
2
|
|
|
3
3
|
Снимок плагина 2026-08-29, поколение `engineApi: 3`.
|
|
4
|
-
Не править: правка фикстуры маскирует слом
|
|
5
|
-
|
|
4
|
+
Не править: правка фикстуры маскирует слом совместимости.
|
|
5
|
+
Новое поколение
|
|
6
6
|
добавляется копированием текущего `../../miniGame`, а не правкой
|
|
7
7
|
этого каталога.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# gen-api4 — ЗАМОРОЖЕНО
|
|
2
2
|
|
|
3
3
|
Снимок плагина 2026-08-29, поколение `engineApi: 4`.
|
|
4
|
-
Не править: правка фикстуры маскирует слом
|
|
5
|
-
|
|
4
|
+
Не править: правка фикстуры маскирует слом совместимости.
|
|
5
|
+
Новое поколение
|
|
6
6
|
добавляется копированием текущего `../../miniGame`, а не правкой
|
|
7
7
|
этого каталога.
|