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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +12 -6
  3. package/bin/vimp-sim.js +2 -2
  4. package/core/Cargo.toml +4 -3
  5. package/package.json +12 -5
  6. package/src/client/components/controller/ClientReports.js +2 -1
  7. package/src/client/components/controller/Games.js +1 -1
  8. package/src/client/components/controller/LobbyAuth.js +6 -2
  9. package/src/client/components/controller/Profile.js +13 -3
  10. package/src/client/components/model/LobbyAuth.js +56 -18
  11. package/src/client/components/model/Profile.js +6 -4
  12. package/src/client/components/view/Auth.js +4 -2
  13. package/src/client/components/view/LobbyAuth.js +7 -7
  14. package/src/client/components/view/Profile.js +0 -1
  15. package/src/client/lib/authApi.js +9 -0
  16. package/src/client/lib/formBuilder.js +9 -8
  17. package/src/client/lib/hostCaps.js +3 -2
  18. package/src/client/lib/sessionSync.js +45 -0
  19. package/src/client/main.js +55 -13
  20. package/src/client/network/TokenRenewer.js +16 -7
  21. package/src/client/network/WebSocketTransport.js +1 -1
  22. package/src/client/session/Membership.js +2 -1
  23. package/src/client/style.css +17 -8
  24. package/src/client/views/includes/lobby.pug +2 -1
  25. package/src/client/views/includes/lobbyAuth.pug +12 -12
  26. package/src/config/authClient.js +3 -3
  27. package/src/config/gameCodes.js +2 -2
  28. package/src/config/lobby.js +2 -3
  29. package/src/config/wsports.js +2 -2
  30. package/src/devtools/builtinScenario.js +3 -3
  31. package/src/devtools/contract/rules/a2-package-scripts.js +4 -3
  32. package/src/devtools/contract/rules/a4-vite-config.js +4 -3
  33. package/src/devtools/contract/rules/a7-package-repository.js +6 -5
  34. package/src/devtools/contract/rules/c4-component-dependencies.js +5 -5
  35. package/src/devtools/invariants.js +1 -1
  36. package/src/devtools/pluginLoader.js +3 -3
  37. package/src/host/PortMachine.js +6 -5
  38. package/src/lib/loadGamePackage.js +1 -1
  39. package/src/lib/packageLink.js +11 -10
  40. package/src/standalone/index.js +5 -5
  41. package/bin/vimp-surface.js +0 -88
  42. package/src/config/env.js +0 -104
  43. package/src/config/master.js +0 -288
  44. package/src/lib/clientIp.js +0 -96
  45. package/src/lib/rateLimiter.js +0 -40
  46. package/src/lib/security.js +0 -45
@@ -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, поэтому панель им не управляет (см. docs/en/client.md)
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.md "Form schema") — движок не выводит контролы из типа
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
- // запись — auth-сервис, перечитывая роль из БД
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-форме) туда не сериализуются (docs/en/plugin-api.md
2569
- // "Form schema"); авторитетный клампинг всё равно в applyRoomOverrides.js
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
- return Math.max(0, expiresAt - this._leadMs - this._now());
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, которые надо держать в голове (см. docs/en/network.md):
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(),
@@ -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
- #lobby-auth-legal,
1121
- #lobby-auth-lifetime {
1122
- color: #aaa;
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-legal a {
1129
- color: #ccc;
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.md "Form schema")
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='14', placeholder='Your nick', autocomplete='off')
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 &copy; #{new Date().getFullYear()} VIMP
@@ -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
- lifetimeId: 'lobby-auth-lifetime',
70
+ versionId: 'lobby-auth-version',
71
71
  restoreId: 'lobby-auth-restore',
72
72
  providersId: 'lobby-auth-providers',
73
73
  restoreErrorId: 'lobby-auth-restore-error',
@@ -1,6 +1,6 @@
1
1
  // коды игровых информ-сообщений (порт GAME_INFORM_DATA, см.
2
- // docs/en/network.md) — общий контракт хоста (SocketManager) и клиента
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],
@@ -213,7 +213,8 @@ export default {
213
213
  // подключён по WebRTC; при смене состава отчёт уходит сразу, с дебаунсом);
214
214
  // minTokenLifetimeMs — вкладка, чей вход истечёт раньше, не объявляет
215
215
  // canHost и отказывается от промоушена (токен предъявляется мастеру
216
- // посреди матча, продления нет); tokenHandoffLeadMs — за столько до
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',
@@ -1,5 +1,5 @@
1
- // Номера портов JSON-канала (docs/en/network.md). Реестр append-only, как и
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.md) — встроенный сценарий не должен демонстрировать
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.md) `type: 1` — триггер, у которого 'up'
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.md). Это не вкусовое
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
- // молчаливую поломку сборки, а не стилистику (docs/en/packaging.md):
6
- // без preserveEntrySignatures default-экспорт плагина вытряхивает
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
- // только отсюда (фолбэка нет, см. lib/packageLink.js): без поля ячейка
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: 'package.json declares repository (the #auth footer links to it)',
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.md «hooks.services»). Незнакомое имя не
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.md).
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.md, не хватало только
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
 
@@ -1,9 +1,10 @@
1
1
  // Хендшейк-машина клиента, вынутая из host.worker.js: автомат по клиентским
2
- // портам 0–10 (см. docs/en/network.md). Изоморфна — ни self, ни postMessage,
3
- // ни DOM: всё, что знает про транспорт, приходит через makeSocket. Поэтому её
4
- // крутит и Worker браузерного хоста, и inline-хост standalone SDK, и
5
- // Node-процесс dedicated-сервера: копий автомата быть не должно, иначе они
6
- // разъедутся ровно так, как разъезжались бы копии createHostRuntime.
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.md). Для боевой игры она обязательна: без ядра
15
+ // docs.vimp.lgick.dev/en/packaging). Для боевой игры она обязательна: без ядра
16
16
  // авторитетную симуляцию крутить нечем.
17
17
 
18
18
  /**
@@ -1,7 +1,7 @@
1
1
  // Ссылка на проект для футеров лобби и формы входа: движок показывает рядом
2
- // npm-версию, и клик по соседней ячейке ведёт в репозиторий этого пакета.
2
+ // npm-версию, и клик по соседней ячейке ведёт на страницу проекта этого пакета.
3
3
  //
4
- // Источник — `repository` его package.json (иначе `homepage`). Фолбэка нет:
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: repository, иначе
20
- * homepage.
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
- // объявленный, но не приводящийся к http(s) repository (пустая строка,
36
- // внутренний git-хост) не повод терять валидный homepage: поле проверяется
37
- // по результату, а не по факту наличия
38
- return normalizeRepository(declared) ?? normalize(homepage);
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
- const scp = /^git@([^:/]+):(.+)$/.exec(url);
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]}`;
@@ -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/client.md, «Error reporting»). По
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.md) либо
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' —