vimp-engine 0.38.0 → 0.38.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/LICENSE +21 -0
- package/README.md +12 -6
- package/bin/vimp-sim.js +2 -2
- package/core/Cargo.toml +4 -3
- package/package.json +12 -5
- package/src/client/components/controller/ClientReports.js +2 -1
- package/src/client/components/controller/Games.js +1 -1
- package/src/client/components/controller/LobbyAuth.js +6 -2
- package/src/client/components/controller/Profile.js +13 -3
- package/src/client/components/model/LobbyAuth.js +56 -18
- package/src/client/components/model/Profile.js +6 -4
- package/src/client/components/view/Auth.js +4 -2
- package/src/client/components/view/LobbyAuth.js +7 -7
- package/src/client/components/view/Profile.js +0 -1
- package/src/client/lib/authApi.js +9 -0
- package/src/client/lib/formBuilder.js +9 -8
- package/src/client/lib/hostCaps.js +3 -2
- package/src/client/lib/sessionSync.js +45 -0
- package/src/client/main.js +55 -13
- package/src/client/network/TokenRenewer.js +16 -7
- package/src/client/network/WebSocketTransport.js +1 -1
- package/src/client/session/Membership.js +2 -1
- package/src/client/style.css +17 -8
- package/src/client/views/includes/lobby.pug +2 -1
- package/src/client/views/includes/lobbyAuth.pug +12 -12
- package/src/config/authClient.js +3 -3
- package/src/config/gameCodes.js +2 -2
- package/src/config/lobby.js +2 -3
- package/src/config/wsports.js +2 -2
- package/src/devtools/builtinScenario.js +3 -3
- package/src/devtools/contract/rules/a2-package-scripts.js +4 -3
- package/src/devtools/contract/rules/a4-vite-config.js +4 -3
- package/src/devtools/contract/rules/a7-package-repository.js +6 -5
- package/src/devtools/contract/rules/c4-component-dependencies.js +5 -5
- package/src/devtools/invariants.js +1 -1
- package/src/devtools/pluginLoader.js +3 -3
- package/src/host/PortMachine.js +6 -5
- package/src/lib/loadGamePackage.js +1 -1
- package/src/lib/packageLink.js +11 -10
- package/src/standalone/index.js +5 -5
- package/bin/vimp-surface.js +0 -88
- package/src/config/env.js +0 -104
- package/src/config/master.js +0 -288
- package/src/lib/clientIp.js +0 -96
- package/src/lib/rateLimiter.js +0 -40
- package/src/lib/security.js +0 -45
package/src/client/main.js
CHANGED
|
@@ -70,6 +70,10 @@ import FpsMeter from './lib/FpsMeter.js';
|
|
|
70
70
|
import HostUnloadGuard from './lib/hostUnloadGuard.js';
|
|
71
71
|
import { createPageReload } from './lib/pageReload.js';
|
|
72
72
|
import { createAuthApi } from './lib/authApi.js';
|
|
73
|
+
import {
|
|
74
|
+
decideSessionStorageChange,
|
|
75
|
+
decideStoredSession,
|
|
76
|
+
} from './lib/sessionSync.js';
|
|
73
77
|
import { createVerifier, challengeOf } from './lib/pkce.js';
|
|
74
78
|
import { createTurnstile } from './lib/turnstile.js';
|
|
75
79
|
import TokenRenewer from './network/TokenRenewer.js';
|
|
@@ -930,7 +934,8 @@ socketMethods[PS_GAME_INFORM_DATA] = data => {
|
|
|
930
934
|
};
|
|
931
935
|
|
|
932
936
|
// проигрывает shimmer-волну по логотипу в начале раунда; логотип живёт вне
|
|
933
|
-
// PanelView.containerId, поэтому панель им не управляет
|
|
937
|
+
// PanelView.containerId, поэтому панель им не управляет
|
|
938
|
+
// (см. docs.vimp.lgick.dev/en/client-api)
|
|
934
939
|
const playLogoRoundStart = () => {
|
|
935
940
|
const logo = document.getElementById('logo');
|
|
936
941
|
|
|
@@ -2178,8 +2183,8 @@ let roomFormFields = new Map();
|
|
|
2178
2183
|
let roomFormDescriptors = [];
|
|
2179
2184
|
|
|
2180
2185
|
// генерирует форму создания комнаты по явной схеме манифеста (roomForm,
|
|
2181
|
-
// docs/en/plugin-api
|
|
2182
|
-
// значения, схема плагина полностью описывает форму
|
|
2186
|
+
// docs.vimp.lgick.dev/en/plugin-api "Form schema") — движок не выводит контролы
|
|
2187
|
+
// из типа значения, схема плагина полностью описывает форму
|
|
2183
2188
|
function populateRoomForm(manifest) {
|
|
2184
2189
|
const container = document.getElementById(lobbyConfig.elems.fieldsId);
|
|
2185
2190
|
|
|
@@ -2469,8 +2474,8 @@ function initLobby() {
|
|
|
2469
2474
|
const games = new GamesCtrl(gamesModel, gamesView);
|
|
2470
2475
|
|
|
2471
2476
|
// кнопку «Moderation» лобби показывает роли admin (master-game-registry,
|
|
2472
|
-
// этап 4). Это подсказка интерфейсу:
|
|
2473
|
-
//
|
|
2477
|
+
// этап 4). Это подсказка интерфейсу: права админа auth-сервис проверяет
|
|
2478
|
+
// сам, по нику из VIMP_ADMIN_NICKS
|
|
2474
2479
|
games.setAdmin(lobbyAuthModel.getRole() === 'admin');
|
|
2475
2480
|
|
|
2476
2481
|
// журнал клиентских ошибок (plan/client-reports, этап 5): кнопку «Errors»
|
|
@@ -2565,13 +2570,13 @@ function initLobby() {
|
|
|
2565
2570
|
hostBtn?.addEventListener('click', async () => {
|
|
2566
2571
|
// валидация (pattern/required/min/max) — единственная граница
|
|
2567
2572
|
// room-формы: она едет клиенту как JSON манифеста, JS-валидаторы
|
|
2568
|
-
// (как в auth-форме) туда не сериализуются
|
|
2569
|
-
// "Form schema"); авторитетный клампинг
|
|
2570
|
-
// (вызывается из host.worker.js при
|
|
2571
|
-
// активации игры: неверная форма не должна
|
|
2572
|
-
// arm() и рисует ошибки, и снимает фильтр «только
|
|
2573
|
-
// это ответ за форму целиком, включая поля, которых
|
|
2574
|
-
// Он же чистит блок, когда ошибок нет
|
|
2573
|
+
// (как в auth-форме) туда не сериализуются
|
|
2574
|
+
// (docs.vimp.lgick.dev/en/plugin-api "Form schema"); авторитетный клампинг
|
|
2575
|
+
// всё равно в applyRoomOverrides.js (вызывается из host.worker.js при
|
|
2576
|
+
// создании комнаты). Проверяем до активации игры: неверная форма не должна
|
|
2577
|
+
// стоить загрузки плагина. arm() и рисует ошибки, и снимает фильтр «только
|
|
2578
|
+
// тронутые поля»: клик — это ответ за форму целиком, включая поля, которых
|
|
2579
|
+
// игрок не касался. Он же чистит блок, когда ошибок нет
|
|
2575
2580
|
if (liveErrors.arm().length) {
|
|
2576
2581
|
return;
|
|
2577
2582
|
}
|
|
@@ -2695,7 +2700,6 @@ if (isLobbyMode) {
|
|
|
2695
2700
|
const tokenRenewer = new TokenRenewer({
|
|
2696
2701
|
getExpiresAt: getTokenExpiresAt,
|
|
2697
2702
|
renew: () => lobbyAuthModel.refresh(),
|
|
2698
|
-
onLost: () => {},
|
|
2699
2703
|
leadMs: authClientConfig.renew.leadMs,
|
|
2700
2704
|
retryMs: authClientConfig.renew.retryMs,
|
|
2701
2705
|
});
|
|
@@ -2765,6 +2769,44 @@ if (isLobbyMode) {
|
|
|
2765
2769
|
diagnostics.warn('engine.session.profileGone', {}, { source: 'client' });
|
|
2766
2770
|
});
|
|
2767
2771
|
|
|
2772
|
+
// другая вкладка сменила личность браузера (регистрация, Restore, выход,
|
|
2773
|
+
// удаление): вкладка вне комнаты перечитывает её перезагрузкой
|
|
2774
|
+
window.addEventListener('storage', event => {
|
|
2775
|
+
if (event.storageArea !== localStorage) {
|
|
2776
|
+
return;
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
const action = decideSessionStorageChange({
|
|
2780
|
+
event,
|
|
2781
|
+
sessionKey: authClientConfig.sessionStorageKey,
|
|
2782
|
+
currentSession: lobbyAuthModel.getSession(),
|
|
2783
|
+
inRoom: Boolean(roomCtx.roomId),
|
|
2784
|
+
});
|
|
2785
|
+
|
|
2786
|
+
if (action === 'reload') {
|
|
2787
|
+
reloadPage();
|
|
2788
|
+
}
|
|
2789
|
+
});
|
|
2790
|
+
|
|
2791
|
+
// страница, восстановленная из bfcache (возврат кнопкой «Назад» от
|
|
2792
|
+
// провайдера OAuth), событий storage не получала: личность сверяется с
|
|
2793
|
+
// хранилищем заново
|
|
2794
|
+
window.addEventListener('pageshow', event => {
|
|
2795
|
+
if (!event.persisted) {
|
|
2796
|
+
return;
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
const action = decideStoredSession({
|
|
2800
|
+
storedSession: localStorage.getItem(authClientConfig.sessionStorageKey),
|
|
2801
|
+
currentSession: lobbyAuthModel.getSession(),
|
|
2802
|
+
inRoom: Boolean(roomCtx.roomId),
|
|
2803
|
+
});
|
|
2804
|
+
|
|
2805
|
+
if (action === 'reload') {
|
|
2806
|
+
reloadPage();
|
|
2807
|
+
}
|
|
2808
|
+
});
|
|
2809
|
+
|
|
2768
2810
|
// возврат с OAuth и dev-сессия прочитаны — чистим только query, hash
|
|
2769
2811
|
// (маршрут #/<gameId>[/<roomId>]) остаётся
|
|
2770
2812
|
if (lobbyAuthModel.takeUrlParams(window.location.search)) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Тихое продление identity-токена: за leadMs до exp запрашивает свежий у
|
|
2
2
|
// сессии устройства. Сбой сети — повтор через retryMs без ограничения числа
|
|
3
|
-
// попыток: сеть вернётся — токен продлится.
|
|
3
|
+
// попыток: сеть вернётся — токен продлится. Запуски идут не чаще раза в
|
|
4
|
+
// retryMs, даже если срок остаётся в окне продления (цикла не будет).
|
|
4
5
|
//
|
|
5
6
|
// Без DOM: срок, продление и таймеры инъектирует владелец (client/main.js).
|
|
6
7
|
|
|
@@ -9,7 +10,6 @@ export default class TokenRenewer {
|
|
|
9
10
|
* @param {Object} options
|
|
10
11
|
* @param {Function} options.getExpiresAt - () → мс эпохи | null.
|
|
11
12
|
* @param {Function} options.renew - () → Promise<'ok' | 'retry' | 'lost'>.
|
|
12
|
-
* @param {Function} options.onLost - сессия потеряна, продлевать нечем.
|
|
13
13
|
* @param {number} options.leadMs
|
|
14
14
|
* @param {number} options.retryMs
|
|
15
15
|
* @param {Function} [options.now]
|
|
@@ -18,7 +18,6 @@ export default class TokenRenewer {
|
|
|
18
18
|
constructor({
|
|
19
19
|
getExpiresAt,
|
|
20
20
|
renew,
|
|
21
|
-
onLost,
|
|
22
21
|
leadMs,
|
|
23
22
|
retryMs,
|
|
24
23
|
now = () => Date.now(),
|
|
@@ -26,12 +25,12 @@ export default class TokenRenewer {
|
|
|
26
25
|
}) {
|
|
27
26
|
this._getExpiresAt = getExpiresAt;
|
|
28
27
|
this._renew = renew;
|
|
29
|
-
this._onLost = onLost;
|
|
30
28
|
this._leadMs = leadMs;
|
|
31
29
|
this._retryMs = retryMs;
|
|
32
30
|
this._now = now;
|
|
33
31
|
this._timers = timers;
|
|
34
32
|
this._timer = null;
|
|
33
|
+
this._lastFireAt = null;
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
// таймер на expiresAt - leadMs, но не раньше «сейчас»; срок неизвестен —
|
|
@@ -54,7 +53,17 @@ export default class TokenRenewer {
|
|
|
54
53
|
return null;
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
|
|
56
|
+
const now = this._now();
|
|
57
|
+
// не раньше retryMs после предыдущего запуска: срок, который и со
|
|
58
|
+
// свежим токеном остаётся в окне продления, не должен завести цикл.
|
|
59
|
+
// Сверху порог ограничен тем же retryMs: часы устройства могли перевести
|
|
60
|
+
// назад, и разность с прошлым запуском уже ничего не значит
|
|
61
|
+
const floor =
|
|
62
|
+
this._lastFireAt === null
|
|
63
|
+
? 0
|
|
64
|
+
: Math.min(this._retryMs, this._lastFireAt + this._retryMs - now);
|
|
65
|
+
|
|
66
|
+
return Math.max(0, floor, expiresAt - this._leadMs - now);
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
_schedule(delay) {
|
|
@@ -69,6 +78,7 @@ export default class TokenRenewer {
|
|
|
69
78
|
|
|
70
79
|
async _fire() {
|
|
71
80
|
this._timer = null;
|
|
81
|
+
this._lastFireAt = this._now();
|
|
72
82
|
|
|
73
83
|
let result;
|
|
74
84
|
|
|
@@ -83,8 +93,7 @@ export default class TokenRenewer {
|
|
|
83
93
|
this.arm();
|
|
84
94
|
} else if (result === 'retry') {
|
|
85
95
|
this._schedule(this._retryMs);
|
|
86
|
-
} else {
|
|
87
|
-
this._onLost();
|
|
88
96
|
}
|
|
97
|
+
// 'lost': таймер не взводится, потерю объявляет модель
|
|
89
98
|
}
|
|
90
99
|
}
|
|
@@ -5,7 +5,7 @@ import Publisher from '../../lib/Publisher.js';
|
|
|
5
5
|
// publisher с 'message'/'close', connect/send/close, — поэтому клиентский код
|
|
6
6
|
// про него не знает.
|
|
7
7
|
//
|
|
8
|
-
// Отличия от WebRTC, которые надо держать в
|
|
8
|
+
// Отличия от WebRTC, которые надо держать в голове:
|
|
9
9
|
// разделения meta/state тут нет, флаг reliable игнорируется. Значит PING/PONG
|
|
10
10
|
// идут надёжным каналом (замер RTT становится замером TCP-пути), а бэкпрешер
|
|
11
11
|
// вместо дропа позиционных кадров живёт на серверной стороне.
|
|
@@ -85,7 +85,8 @@ export default class Membership {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
// хватит ли срока входа на роль хоста: register_host/reclaim_host
|
|
88
|
-
// предъявляют токен мастеру посреди
|
|
88
|
+
// предъявляют токен мастеру посреди матча. Страховка на случай, когда
|
|
89
|
+
// тихое продление токена не удаётся
|
|
89
90
|
tokenAllowsHostRole() {
|
|
90
91
|
return tokenAllowsHosting(
|
|
91
92
|
this._getTokenExpiresAt(),
|
package/src/client/style.css
CHANGED
|
@@ -589,6 +589,7 @@ body.hide-cursor {
|
|
|
589
589
|
держится в одном месте (#lobby-footer лежит в секции lobby ниже только
|
|
590
590
|
разметкой) */
|
|
591
591
|
#auth-link,
|
|
592
|
+
#lobby-auth-footer,
|
|
592
593
|
#lobby-footer {
|
|
593
594
|
height: 15px;
|
|
594
595
|
text-shadow: none;
|
|
@@ -601,15 +602,18 @@ body.hide-cursor {
|
|
|
601
602
|
padding: 0 5px;
|
|
602
603
|
}
|
|
603
604
|
|
|
605
|
+
#lobby-auth-footer,
|
|
604
606
|
#lobby-footer {
|
|
605
607
|
font-size: 10px;
|
|
606
608
|
padding: 0;
|
|
607
609
|
}
|
|
608
610
|
|
|
611
|
+
#lobby-auth-footer p,
|
|
609
612
|
#lobby-footer p {
|
|
610
613
|
margin: 0 5px;
|
|
611
614
|
}
|
|
612
615
|
|
|
616
|
+
#lobby-auth-footer p:not(:last-child)::after,
|
|
613
617
|
#lobby-footer p:not(:last-child)::after {
|
|
614
618
|
content: '|';
|
|
615
619
|
padding-left: 10px;
|
|
@@ -624,6 +628,7 @@ body.hide-cursor {
|
|
|
624
628
|
}
|
|
625
629
|
|
|
626
630
|
#auth-link p a,
|
|
631
|
+
#lobby-auth-footer p a,
|
|
627
632
|
#lobby-footer p a {
|
|
628
633
|
color: #ff0;
|
|
629
634
|
}
|
|
@@ -1117,16 +1122,20 @@ body.hide-cursor {
|
|
|
1117
1122
|
margin: 8px auto;
|
|
1118
1123
|
}
|
|
1119
1124
|
|
|
1120
|
-
|
|
1121
|
-
#lobby-auth-
|
|
1122
|
-
|
|
1123
|
-
font-size: 11px;
|
|
1124
|
-
line-height: 14px;
|
|
1125
|
-
margin: 8px 0;
|
|
1125
|
+
/* виджет Turnstile шириной 300 px не влезает в карточку 256 px */
|
|
1126
|
+
#lobby-auth-welcome {
|
|
1127
|
+
width: 300px;
|
|
1126
1128
|
}
|
|
1127
1129
|
|
|
1128
|
-
#lobby-auth-
|
|
1129
|
-
|
|
1130
|
+
#lobby-auth-turnstile {
|
|
1131
|
+
display: flex;
|
|
1132
|
+
justify-content: center;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/* футер вне рамки: по центру под карточкой */
|
|
1136
|
+
#lobby-auth-footer {
|
|
1137
|
+
justify-content: center;
|
|
1138
|
+
margin: 10px auto 0;
|
|
1130
1139
|
}
|
|
1131
1140
|
|
|
1132
1141
|
/* нейтральная плашка уведомления (профиль не найден, выход, удаление) */
|
|
@@ -32,7 +32,8 @@ div#lobby(style='display:none')
|
|
|
32
32
|
label.form-label(for='lobby-game')= 'Select game'
|
|
33
33
|
select#lobby-game.field-select
|
|
34
34
|
//- поля настроек комнаты генерируются по явной схеме roomForm
|
|
35
|
-
//- манифеста активной игры (docs/en/plugin-api
|
|
35
|
+
//- манифеста активной игры (docs.vimp.lgick.dev/en/plugin-api "Form
|
|
36
|
+
//- schema")
|
|
36
37
|
div#lobby-fields
|
|
37
38
|
//- ошибки формы (formBuilder.renderFormErrors) и отказ загрузки
|
|
38
39
|
//- плагина выбранной игры — строкой в форме, а не в #tech-informer:
|
|
@@ -9,24 +9,13 @@ div#lobby-auth
|
|
|
9
9
|
|
|
10
10
|
div#lobby-auth-welcome.card(style='display:none')
|
|
11
11
|
h2= 'Welcome to VIMP!'
|
|
12
|
-
p= 'Pick a nick and play. No e-mail or password needed.'
|
|
13
12
|
p#lobby-auth-notice.form-notice
|
|
14
|
-
input#lobby-auth-nick-input.field-text(type='text', maxlength='
|
|
13
|
+
input#lobby-auth-nick-input.field-text(type='text', maxlength='15', placeholder='Your nick', autocomplete='off')
|
|
15
14
|
div#lobby-auth-nick-error.form-error
|
|
16
15
|
input#lobby-auth-turnstile-retry(type='button', value='Retry', style='display:none')
|
|
17
16
|
//- контейнер виджета Turnstile: виден, только когда проверке нужен клик
|
|
18
17
|
div#lobby-auth-turnstile
|
|
19
18
|
input#lobby-auth-register(type='button', value='Create profile')
|
|
20
|
-
p#lobby-auth-legal
|
|
21
|
-
| By creating a profile you agree to the
|
|
22
|
-
|
|
|
23
|
-
a#lobby-auth-terms(target='_blank', rel='noopener') Terms of Service
|
|
24
|
-
|
|
|
25
|
-
| and the
|
|
26
|
-
|
|
|
27
|
-
a#lobby-auth-privacy(target='_blank', rel='noopener') Privacy Policy
|
|
28
|
-
| .
|
|
29
|
-
p#lobby-auth-lifetime
|
|
30
19
|
hr.lobby-divider
|
|
31
20
|
input#lobby-auth-restore(type='button', value='Restore profile')
|
|
32
21
|
div#lobby-auth-providers(style='display:none')
|
|
@@ -35,3 +24,14 @@ div#lobby-auth
|
|
|
35
24
|
input.lobby-auth-provider(type='button', data-provider='discord', value='Continue with Discord')
|
|
36
25
|
input.lobby-auth-provider(type='button', data-provider='github', value='Continue with GitHub')
|
|
37
26
|
div#lobby-auth-restore-error.form-error
|
|
27
|
+
|
|
28
|
+
//- футер под карточкой Welcome: ссылки заполняет LobbyAuthView,
|
|
29
|
+
//- версию движка — он же (engineVersion.js)
|
|
30
|
+
div#lobby-auth-footer(style='display:none')
|
|
31
|
+
p
|
|
32
|
+
a#lobby-auth-terms(target='_blank', rel='noopener') Terms of Service
|
|
33
|
+
| ·
|
|
34
|
+
|
|
|
35
|
+
a#lobby-auth-privacy(target='_blank', rel='noopener') Privacy Policy
|
|
36
|
+
p#lobby-auth-version
|
|
37
|
+
p © #{new Date().getFullYear()} VIMP
|
package/src/config/authClient.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Конфиг клиента профиля лобби. Бандлится в клиентскую сборку, как
|
|
2
2
|
// config/lobby.js — auth-сервис живёт на отдельном домене/порте, поэтому
|
|
3
|
-
// serviceUrl подставляется под конкретный деплой при
|
|
4
|
-
// docs/en/auth.md).
|
|
3
|
+
// serviceUrl подставляется под конкретный деплой при сборке.
|
|
5
4
|
export default {
|
|
6
5
|
// базовый URL центрального auth-сервиса (packages/auth). Дефолт — dev-порт
|
|
7
6
|
// из packages/auth/src/config/auth.js; в проде подставляется Vite'ом из
|
|
@@ -59,6 +58,7 @@ export default {
|
|
|
59
58
|
statusTextId: 'lobby-auth-status-text',
|
|
60
59
|
statusRetryId: 'lobby-auth-status-retry',
|
|
61
60
|
welcomeId: 'lobby-auth-welcome',
|
|
61
|
+
footerId: 'lobby-auth-footer',
|
|
62
62
|
noticeId: 'lobby-auth-notice',
|
|
63
63
|
nickInputId: 'lobby-auth-nick-input',
|
|
64
64
|
nickErrorId: 'lobby-auth-nick-error',
|
|
@@ -67,7 +67,7 @@ export default {
|
|
|
67
67
|
registerId: 'lobby-auth-register',
|
|
68
68
|
termsId: 'lobby-auth-terms',
|
|
69
69
|
privacyId: 'lobby-auth-privacy',
|
|
70
|
-
|
|
70
|
+
versionId: 'lobby-auth-version',
|
|
71
71
|
restoreId: 'lobby-auth-restore',
|
|
72
72
|
providersId: 'lobby-auth-providers',
|
|
73
73
|
restoreErrorId: 'lobby-auth-restore-error',
|
package/src/config/gameCodes.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// коды игровых информ-сообщений (порт GAME_INFORM_DATA, см.
|
|
2
|
-
// docs/en/
|
|
3
|
-
// (main.js): значение не должно расходиться между ними молча
|
|
2
|
+
// docs.vimp.lgick.dev/en/snapshot) — общий контракт хоста (SocketManager) и
|
|
3
|
+
// клиента (main.js): значение не должно расходиться между ними молча
|
|
4
4
|
export default {
|
|
5
5
|
winnerTeam: [0],
|
|
6
6
|
roundStart: [1],
|
package/src/config/lobby.js
CHANGED
|
@@ -213,7 +213,8 @@ export default {
|
|
|
213
213
|
// подключён по WebRTC; при смене состава отчёт уходит сразу, с дебаунсом);
|
|
214
214
|
// minTokenLifetimeMs — вкладка, чей вход истечёт раньше, не объявляет
|
|
215
215
|
// canHost и отказывается от промоушена (токен предъявляется мастеру
|
|
216
|
-
// посреди
|
|
216
|
+
// посреди матча; обычно его заранее продлевает TokenRenewer, порог работает,
|
|
217
|
+
// пока продление не удаётся); tokenHandoffLeadMs — за столько до
|
|
217
218
|
// истечения входа хост с бетой сам передаёт роль (плановая передача на
|
|
218
219
|
// границе раунда); tokenHandoffRetryMs — через сколько повторить её, если
|
|
219
220
|
// беты ещё нет, идёт эстафета Worker'ов или передача сорвалась (повторы —
|
|
@@ -459,7 +460,6 @@ export default {
|
|
|
459
460
|
closeBtnId: 'profile-close',
|
|
460
461
|
nickId: 'profile-nick',
|
|
461
462
|
statusId: 'profile-status',
|
|
462
|
-
linkCardId: 'profile-link',
|
|
463
463
|
benefitsId: 'profile-link-benefits',
|
|
464
464
|
providersId: 'profile-providers',
|
|
465
465
|
consentId: 'profile-consent',
|
|
@@ -475,7 +475,6 @@ export default {
|
|
|
475
475
|
sessionErrorId: 'profile-session-error',
|
|
476
476
|
exportBtnId: 'profile-export',
|
|
477
477
|
exportErrorId: 'profile-export-error',
|
|
478
|
-
deleteCardId: 'profile-delete',
|
|
479
478
|
deleteAdminNoteId: 'profile-delete-admin',
|
|
480
479
|
deleteStartBtnId: 'profile-delete-start',
|
|
481
480
|
deleteConfirmId: 'profile-delete-confirm',
|
package/src/config/wsports.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Номера портов JSON-канала (docs/en/
|
|
2
|
-
// остальные словари плагинной поверхности (этап 3 плана
|
|
1
|
+
// Номера портов JSON-канала (docs.vimp.lgick.dev/en/snapshot). Реестр
|
|
2
|
+
// append-only, как и остальные словари плагинной поверхности (этап 3 плана
|
|
3
3
|
// plugin-forward-compat): номер НИКОГДА не переиспользуется и не
|
|
4
4
|
// перенумеровывается — уже опубликованная сборка игры шлёт и слушает те
|
|
5
5
|
// номера, что видела при сборке, и её dist больше никто не тронет.
|
|
@@ -13,8 +13,8 @@ import { isFixture } from './pluginLoader.js';
|
|
|
13
13
|
// Ввод начинается на 40-м тике, а не сразу: кадр спавна с force_reset
|
|
14
14
|
// приходит примерно на interpolation.delay (движковый дефолт — 100 мс, тут
|
|
15
15
|
// ~3× запаса) позже входа и чистит удержанные клавиши предиктора
|
|
16
|
-
// (docs/en/debugging
|
|
17
|
-
// ровно ту ловушку, от которой предостерегает документация
|
|
16
|
+
// (docs.vimp.lgick.dev/en/debugging) — встроенный сценарий не должен
|
|
17
|
+
// демонстрировать ровно ту ловушку, от которой предостерегает документация
|
|
18
18
|
const INPUT_DOWN_TICK = 40;
|
|
19
19
|
const INPUT_UP_TICK = 100;
|
|
20
20
|
const TICKS = 120;
|
|
@@ -26,7 +26,7 @@ const FIXTURE_UNUSED_KEYS = ['e1'];
|
|
|
26
26
|
const firstKey = dict => Object.keys(dict ?? {})[0];
|
|
27
27
|
|
|
28
28
|
// Клавиша для смоука обязана быть удерживаемой: по конвенции playerKeys
|
|
29
|
-
// (docs/en/plugin-api
|
|
29
|
+
// (docs.vimp.lgick.dev/en/plugin-api) `type: 1` — триггер, у которого 'up'
|
|
30
30
|
// игнорируется, и «нажал — отпустил» не даёт ни удержания, ни движения.
|
|
31
31
|
// Порядок объявления в playerKeys контрактом не задан, поэтому берём первую
|
|
32
32
|
// без type; если игра объявила одни триггеры — первую любую, смоук всё равно
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { ERROR, skip, verdict } from '../result.js';
|
|
2
2
|
|
|
3
|
-
// Набор npm-скриптов пакета игры (docs/en/packaging
|
|
4
|
-
// требование: инструкции движка и CI сгенерированной игры зовут их по
|
|
5
|
-
// а отсутствующий core:build:node тихо оставляет headless-прогон без
|
|
3
|
+
// Набор npm-скриптов пакета игры (docs.vimp.lgick.dev/en/packaging). Это не
|
|
4
|
+
// вкусовое требование: инструкции движка и CI сгенерированной игры зовут их по
|
|
5
|
+
// имени, а отсутствующий core:build:node тихо оставляет headless-прогон без
|
|
6
|
+
// ядра.
|
|
6
7
|
const REQUIRED = [
|
|
7
8
|
'build',
|
|
8
9
|
'build:client',
|
|
@@ -2,9 +2,10 @@ import { ERROR, skip, verdict } from '../result.js';
|
|
|
2
2
|
|
|
3
3
|
// vite.config.js проверяется как текст, а не импортом: конфиг —
|
|
4
4
|
// функция от mode и вне Vite не исполняется. Каждый пункт здесь ловит
|
|
5
|
-
// молчаливую поломку сборки, а не стилистику
|
|
6
|
-
// без preserveEntrySignatures
|
|
7
|
-
// tree-shaking, а build.lib всегда инлайнит
|
|
5
|
+
// молчаливую поломку сборки, а не стилистику
|
|
6
|
+
// (docs.vimp.lgick.dev/en/packaging): без preserveEntrySignatures
|
|
7
|
+
// default-экспорт плагина вытряхивает tree-shaking, а build.lib всегда инлайнит
|
|
8
|
+
// ассеты.
|
|
8
9
|
const REQUIRED = [
|
|
9
10
|
[/emptyOutDir\s*:\s*false/, 'emptyOutDir: false (two builds share dist/)'],
|
|
10
11
|
[/assetsInlineLimit\s*:\s*0/, 'assetsInlineLimit: 0'],
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { WARN, skip, verdict } from '../result.js';
|
|
2
2
|
import { resolveProjectUrl } from '../../../lib/packageLink.js';
|
|
3
3
|
|
|
4
|
-
// Игра без объявленного
|
|
5
|
-
// Но движок показывает ссылку на проект в футере формы входа, и берёт
|
|
6
|
-
// только
|
|
7
|
-
// футера у игроков останется пустой, а заметить это без подсказки негде.
|
|
4
|
+
// Игра без объявленного homepage/repository собирается и играется — это не
|
|
5
|
+
// отказ. Но движок показывает ссылку на проект в футере формы входа, и берёт
|
|
6
|
+
// он её только из этих полей (фолбэка нет, см. lib/packageLink.js): без них
|
|
7
|
+
// ячейка футера у игроков останется пустой, а заметить это без подсказки негде.
|
|
8
8
|
// Отсюда warn — под --strict он становится ошибкой.
|
|
9
9
|
export default {
|
|
10
10
|
id: 'A7',
|
|
11
11
|
name: 'packageRepository',
|
|
12
12
|
level: WARN,
|
|
13
|
-
title:
|
|
13
|
+
title:
|
|
14
|
+
'package.json declares homepage or repository (the #auth footer links to it)',
|
|
14
15
|
|
|
15
16
|
check(ctx) {
|
|
16
17
|
if (!ctx.pkg) {
|
|
@@ -4,11 +4,11 @@ import { SERVICES } from '../../../config/clientServices.js';
|
|
|
4
4
|
// Движковый пул сервисов держит реестр (config/clientServices.js), но пул
|
|
5
5
|
// ими не исчерпывается: игра доливает туда свои через
|
|
6
6
|
// ClientPlugin.hooks.services(core) — например геометрию предсказанной
|
|
7
|
-
// динамики карты (docs/en/plugin-api
|
|
8
|
-
// ошибка для движка: part получает undefined и рисует пустоту
|
|
9
|
-
// assetsBase выглядит как чистый холст без единой строки в консоли,
|
|
10
|
-
// этого правило и существует. Правило остаётся ERROR: оно работает на
|
|
11
|
-
// разработки игры, а не в рантайме.
|
|
7
|
+
// динамики карты (docs.vimp.lgick.dev/en/plugin-api «hooks.services»).
|
|
8
|
+
// Незнакомое имя не ошибка для движка: part получает undefined и рисует пустоту
|
|
9
|
+
// — карта без assetsBase выглядит как чистый холст без единой строки в консоли,
|
|
10
|
+
// ради этого правило и существует. Правило остаётся ERROR: оно работает на
|
|
11
|
+
// этапе разработки игры, а не в рантайме.
|
|
12
12
|
//
|
|
13
13
|
// Что именно возвращает `hooks.services(core)`, статически не видно — хук
|
|
14
14
|
// требует живого ядра. Поэтому у плагина есть необязательное поле
|
|
@@ -484,7 +484,7 @@ function predictionDrift({ clients, scenario }) {
|
|
|
484
484
|
// Компоненты player-блока — игровая раскладка, движок знает только их
|
|
485
485
|
// порядок, поэтому нарушение адресуется индексом. Исключение — уровень 0
|
|
486
486
|
// (source 'camera'): там движок сравнивает ровно мировые x/y, и называть их
|
|
487
|
-
// «#0/#1» значит прятать причину (см. docs/en/debugging
|
|
487
|
+
// «#0/#1» значит прятать причину (см. docs.vimp.lgick.dev/en/debugging).
|
|
488
488
|
const CAMERA_COMPONENTS = ['x', 'y'];
|
|
489
489
|
|
|
490
490
|
function formatDivergence(record) {
|
|
@@ -12,9 +12,9 @@ import { loadGamePackage } from '../lib/loadGamePackage.js';
|
|
|
12
12
|
// 3. ничего — фикстура miniGame из этого репозитория.
|
|
13
13
|
//
|
|
14
14
|
// entries.wasmNode — новое ОПЦИОНАЛЬНОЕ поле манифеста: конвенция
|
|
15
|
-
// core/pkg-node/ уже описана в docs/en/packaging
|
|
16
|
-
// пути в манифесте. Его отсутствие не ошибка — просто игра не
|
|
17
|
-
// headless своим настоящим ядром.
|
|
15
|
+
// core/pkg-node/ уже описана в docs.vimp.lgick.dev/en/packaging, не хватало
|
|
16
|
+
// только пути в манифесте. Его отсутствие не ошибка — просто игра не
|
|
17
|
+
// прогоняется headless своим настоящим ядром.
|
|
18
18
|
|
|
19
19
|
const FIXTURE_DIR = new URL('../../tests/fixtures/miniGame/', import.meta.url);
|
|
20
20
|
|
package/src/host/PortMachine.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Хендшейк-машина клиента, вынутая из host.worker.js: автомат по клиентским
|
|
2
|
-
// портам
|
|
3
|
-
// ни DOM: всё, что знает про транспорт, приходит через
|
|
4
|
-
// крутит и Worker браузерного хоста, и inline-хост
|
|
5
|
-
// Node-процесс dedicated-сервера: копий автомата быть не
|
|
6
|
-
// разъедутся ровно так, как разъезжались бы копии
|
|
2
|
+
// портам (таблица портов — docs.vimp.lgick.dev/en/snapshot). Изоморфна — ни
|
|
3
|
+
// self, ни postMessage, ни DOM: всё, что знает про транспорт, приходит через
|
|
4
|
+
// makeSocket. Поэтому её крутит и Worker браузерного хоста, и inline-хост
|
|
5
|
+
// standalone SDK, и Node-процесс dedicated-сервера: копий автомата быть не
|
|
6
|
+
// должно, иначе они разъедутся ровно так, как разъезжались бы копии
|
|
7
|
+
// createHostRuntime.
|
|
7
8
|
|
|
8
9
|
import wsports from '../config/wsports.js';
|
|
9
10
|
import closeCodes from '../config/closeCodes.js';
|
|
@@ -12,7 +12,7 @@ import { ENGINE_CAPABILITIES } from './capabilities.js';
|
|
|
12
12
|
// должен зависеть от отладочного контура (граница из CLAUDE.md).
|
|
13
13
|
//
|
|
14
14
|
// entries.wasmNode — node-сборка ядра игры (конвенция core/pkg-node,
|
|
15
|
-
// docs/en/packaging
|
|
15
|
+
// docs.vimp.lgick.dev/en/packaging). Для боевой игры она обязательна: без ядра
|
|
16
16
|
// авторитетную симуляцию крутить нечем.
|
|
17
17
|
|
|
18
18
|
/**
|
package/src/lib/packageLink.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Ссылка на проект для футеров лобби и формы входа: движок показывает рядом
|
|
2
|
-
// npm-версию, и клик по соседней ячейке ведёт
|
|
2
|
+
// npm-версию, и клик по соседней ячейке ведёт на страницу проекта этого пакета.
|
|
3
3
|
//
|
|
4
|
-
// Источник — `
|
|
4
|
+
// Источник — `homepage` его package.json (иначе `repository`). Фолбэка нет:
|
|
5
5
|
// пакет, не объявивший ни того, ни другого, ссылки не получает, и ячейка
|
|
6
6
|
// футера остаётся пустой. Это сознательно — прежний фолбэк на страницу пакета
|
|
7
7
|
// в npm давал разнобой («GitHub» у одной игры, «npm» у другой) и молча
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
// (client/lib/engineVersion.js).
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Адрес проекта из package.json, приведённый к https:
|
|
20
|
-
*
|
|
19
|
+
* Адрес проекта из package.json, приведённый к https: homepage, иначе
|
|
20
|
+
* repository.
|
|
21
21
|
* @param {Object} pkg - разобранный package.json
|
|
22
22
|
* @returns {string|null} https-URL либо null, если поля нет или это не адрес
|
|
23
23
|
*/
|
|
@@ -32,10 +32,10 @@ export function resolveProjectUrl(pkg) {
|
|
|
32
32
|
const declared =
|
|
33
33
|
typeof repository === 'string' ? repository : repository?.url;
|
|
34
34
|
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
return
|
|
35
|
+
// homepage важнее repository: поле repository обязано называть
|
|
36
|
+
// репозиторий публикации (его сверяет npm Trusted Publishing), и у пакета
|
|
37
|
+
// из закрытого репозитория такая ссылка отдаёт игроку 404
|
|
38
|
+
return normalize(homepage) ?? normalizeRepository(declared);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
@@ -94,8 +94,9 @@ function normalize(raw) {
|
|
|
94
94
|
|
|
95
95
|
url = url.replace(/^git\+/, '');
|
|
96
96
|
|
|
97
|
-
// scp-форма git@host:user/repo
|
|
98
|
-
|
|
97
|
+
// scp-форма git@host:user/repo (в том числе с лишним ssh://, как её
|
|
98
|
+
// нередко пишет npm) — двоеточие здесь разделитель, не порт
|
|
99
|
+
const scp = /^(?:ssh:\/\/)?git@([^:/]+):(.+)$/.exec(url);
|
|
99
100
|
|
|
100
101
|
if (scp) {
|
|
101
102
|
url = `https://${scp[1]}/${scp[2]}`;
|
package/src/standalone/index.js
CHANGED
|
@@ -44,7 +44,7 @@ import { ensureGameShell } from '../client/views/gameShell.js';
|
|
|
44
44
|
* @param {boolean} [options.devMode] - room.isDevMode: рекордер и хостовый
|
|
45
45
|
* CONSOLE-лог.
|
|
46
46
|
* @param {string|null} [options.reportUrl] - Адрес приёма журнала клиентских
|
|
47
|
-
* ошибок (`POST`, формат — docs/en/
|
|
47
|
+
* ошибок (`POST`, формат — docs.vimp.lgick.dev/en/standalone). По
|
|
48
48
|
* умолчанию `null`: в solo нет бокса, и отчёты не отправляются.
|
|
49
49
|
* @returns {Promise<Object>} `{ stop() }` — останов матча.
|
|
50
50
|
*/
|
|
@@ -71,10 +71,10 @@ export async function startStandaloneGame({
|
|
|
71
71
|
// возможность, которой в этой сборке движка нет, иначе всплыла бы где-то
|
|
72
72
|
// в середине хендшейка непрозрачной ошибкой. Мастера тут нет, а значит нет
|
|
73
73
|
// и GameManifest с его `requires` — источником служат сами половины
|
|
74
|
-
// плагина (необязательное поле `requires`, docs/en/plugin-api
|
|
75
|
-
// опция вызова, если встраивающий знает лучше. Одна проверка вместо
|
|
76
|
-
// сообщение адресовано разработчику встраивания, делить его по
|
|
77
|
-
// незачем
|
|
74
|
+
// плагина (необязательное поле `requires`, docs.vimp.lgick.dev/en/plugin-api)
|
|
75
|
+
// либо опция вызова, если встраивающий знает лучше. Одна проверка вместо
|
|
76
|
+
// двух: сообщение адресовано разработчику встраивания, делить его по
|
|
77
|
+
// половинам незачем
|
|
78
78
|
requireCompatible({
|
|
79
79
|
id: hostPlugin.id,
|
|
80
80
|
// mergeRequires, а не спред: строка вместо массива ('accolades' —
|