vimp-engine 0.7.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -4
- package/src/client/InputListener.js +36 -0
- package/src/client/SoundManager.js +498 -0
- package/src/client/boot.js +113 -0
- package/src/client/components/controller/Auth.js +44 -0
- package/src/client/components/controller/CanvasManager.js +26 -0
- package/src/client/components/controller/Chat.js +56 -0
- package/src/client/components/controller/Controls.js +51 -0
- package/src/client/components/controller/Game.js +37 -0
- package/src/client/components/controller/Lobby.js +81 -0
- package/src/client/components/controller/LobbyAuth.js +41 -0
- package/src/client/components/controller/Panel.js +23 -0
- package/src/client/components/controller/Stat.js +33 -0
- package/src/client/components/controller/Vote.js +55 -0
- package/src/client/components/model/Auth.js +92 -0
- package/src/client/components/model/CanvasManager.js +319 -0
- package/src/client/components/model/Chat.js +119 -0
- package/src/client/components/model/Controls.js +120 -0
- package/src/client/components/model/Game.js +172 -0
- package/src/client/components/model/Lobby.js +226 -0
- package/src/client/components/model/LobbyAuth.js +161 -0
- package/src/client/components/model/Panel.js +70 -0
- package/src/client/components/model/Stat.js +77 -0
- package/src/client/components/model/Vote.js +216 -0
- package/src/client/components/view/Auth.js +167 -0
- package/src/client/components/view/CanvasManager.js +44 -0
- package/src/client/components/view/Chat.js +90 -0
- package/src/client/components/view/Controls.js +30 -0
- package/src/client/components/view/Game.js +48 -0
- package/src/client/components/view/Lobby.js +303 -0
- package/src/client/components/view/LobbyAuth.js +111 -0
- package/src/client/components/view/Panel.js +314 -0
- package/src/client/components/view/Stat.js +238 -0
- package/src/client/components/view/Vote.js +108 -0
- package/src/client/debug.js +158 -0
- package/src/client/lib/autostart.js +61 -0
- package/src/client/lib/contextTracker.js +46 -0
- package/src/client/lib/formBuilder.js +280 -0
- package/src/client/lib/hostGate.js +16 -0
- package/src/client/main.js +2016 -0
- package/src/client/network/HostConnectionManager.js +196 -0
- package/src/client/network/HostController.js +422 -0
- package/src/client/network/InlineHostBridge.js +152 -0
- package/src/client/network/LoopbackTransport.js +51 -0
- package/src/client/network/SignalingClient.js +146 -0
- package/src/client/network/WebRtcManager.js +147 -0
- package/src/client/network/WebSocketTransport.js +78 -0
- package/src/client/network/policyClose.js +54 -0
- package/src/client/network/workerSupport.js +34 -0
- package/src/client/providers/BakingProvider.js +88 -0
- package/src/client/providers/DependencyProvider.js +41 -0
- package/src/client/style.css +898 -0
- package/src/client/views/gameShell.js +190 -0
- package/src/client/views/includes/auth.pug +20 -0
- package/src/client/views/includes/chat.pug +5 -0
- package/src/client/views/includes/informer.pug +2 -0
- package/src/client/views/includes/lobby.pug +48 -0
- package/src/client/views/includes/lobbyAuth.pug +19 -0
- package/src/client/views/includes/panel.pug +3 -0
- package/src/client/views/includes/stat.pug +2 -0
- package/src/client/views/index.pug +8 -0
- package/src/config/closeCodes.js +18 -0
- package/src/config/env.js +61 -0
- package/src/devtools/ScenarioRunner.js +3 -8
- package/src/devtools/pluginLoader.js +8 -93
- package/src/host/HostGame.js +41 -3
- package/src/host/PortMachine.js +294 -0
- package/src/host/host.worker.js +29 -246
- package/src/host/identity.js +100 -0
- package/src/lib/clientIp.js +39 -0
- package/src/lib/loadGamePackage.js +122 -0
- package/src/lib/offlinePlayerData.js +18 -0
- package/src/standalone/index.js +125 -0
- package/tests/fixtures/miniGame/config/game.js +4 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// DOM-каркас игрового интерфейса в коде (Этап 2 плана standalone-sdk).
|
|
2
|
+
// В прод-сборке движка разметку даёт pug (./includes/*.pug), но standalone
|
|
3
|
+
// SDK встраивается в чужую страницу репозитория игры, где никакого pug нет —
|
|
4
|
+
// а движковые модули (Auth, Chat, Panel, Stat, информеры) ищут элементы по
|
|
5
|
+
// фиксированным id. Поэтому каркас должен уметь собираться и без сборщика.
|
|
6
|
+
//
|
|
7
|
+
// Функция идемпотентна: существующие элементы не трогает и не переносит,
|
|
8
|
+
// поэтому в lobby-режиме (разметка уже в документе) не делает ничего.
|
|
9
|
+
// Второй источник разметки обязан совпадать с pug — расхождение ловит
|
|
10
|
+
// тест паритета (tests/client/gameShell.test.js).
|
|
11
|
+
//
|
|
12
|
+
// Здесь нет #vote (его создаёт в рантайме components/view/Vote.js) и нет
|
|
13
|
+
// канвасов: их размеры приходят в CONFIG_DATA — см. ensureCanvas().
|
|
14
|
+
|
|
15
|
+
const SHELL = [
|
|
16
|
+
{
|
|
17
|
+
tag: 'div',
|
|
18
|
+
id: 'panel',
|
|
19
|
+
children: [{ tag: 'h1', id: 'logo', text: 'VIMP' }],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
tag: 'div',
|
|
23
|
+
id: 'chat',
|
|
24
|
+
children: [
|
|
25
|
+
{ tag: 'div', id: 'chat-box' },
|
|
26
|
+
// maxlength — UX-подсказка; авторитетный лимит на хосте (chatMaxLength)
|
|
27
|
+
{
|
|
28
|
+
tag: 'input',
|
|
29
|
+
id: 'cmd',
|
|
30
|
+
attrs: { type: 'text', maxlength: '60', autocomplete: 'off' },
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
{ tag: 'div', id: 'stat' },
|
|
35
|
+
{
|
|
36
|
+
tag: 'div',
|
|
37
|
+
id: 'auth',
|
|
38
|
+
children: [
|
|
39
|
+
{
|
|
40
|
+
tag: 'div',
|
|
41
|
+
id: 'auth-form',
|
|
42
|
+
attrs: { class: 'card' },
|
|
43
|
+
children: [
|
|
44
|
+
// заголовок и help-секции заполняет AuthView из authSchema.texts
|
|
45
|
+
{ tag: 'h2', id: 'auth-title' },
|
|
46
|
+
{ tag: 'div', id: 'auth-error' },
|
|
47
|
+
{ tag: 'div', id: 'auth-fields' },
|
|
48
|
+
{
|
|
49
|
+
tag: 'input',
|
|
50
|
+
id: 'auth-enter',
|
|
51
|
+
attrs: { type: 'button', autofocus: '', value: 'Start' },
|
|
52
|
+
},
|
|
53
|
+
{ tag: 'div', id: 'auth-informs' },
|
|
54
|
+
{
|
|
55
|
+
tag: 'div',
|
|
56
|
+
id: 'auth-link',
|
|
57
|
+
children: [
|
|
58
|
+
{
|
|
59
|
+
tag: 'p',
|
|
60
|
+
children: [
|
|
61
|
+
{
|
|
62
|
+
tag: 'a',
|
|
63
|
+
attrs: { href: 'https://github.com/lgick/vimp' },
|
|
64
|
+
text: 'GitHub',
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
{ tag: 'p', text: `© ${new Date().getFullYear()} VIMP` },
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
{ tag: 'div', id: 'game-informer' },
|
|
76
|
+
{ tag: 'div', id: 'tech-informer' },
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
// класс-маркер: правило `.vimp-shell > *` в style.css держит экраны скрытыми
|
|
80
|
+
// до того, как их покажет свой модуль. В прод-сборке контейнер — body, и
|
|
81
|
+
// правило совпадает с прежним `body > *`; в SDK это чужой div
|
|
82
|
+
export const SHELL_CLASS = 'vimp-shell';
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Достраивает недостающие контейнеры игрового интерфейса.
|
|
86
|
+
*
|
|
87
|
+
* Контейнер обязан быть полноэкранным и позиционированным
|
|
88
|
+
* (`position: relative`): #panel, #stat и создаваемый в рантайме #vote —
|
|
89
|
+
* `position: absolute`, и их containing block — ближайший позиционированный
|
|
90
|
+
* предок.
|
|
91
|
+
* @param {HTMLElement} [container] - Точка монтирования (по умолчанию body).
|
|
92
|
+
* @returns {HTMLElement} Контейнер.
|
|
93
|
+
*/
|
|
94
|
+
export function ensureGameShell(container = document.body) {
|
|
95
|
+
container.classList.add(SHELL_CLASS);
|
|
96
|
+
buildNodes(SHELL, container, false);
|
|
97
|
+
|
|
98
|
+
return container;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Полотно игры по id из конфига канвасов (CONFIG_DATA). Уже размещённый в
|
|
103
|
+
* документе элемент переиспользуется как есть — если игра положила
|
|
104
|
+
* <canvas id="vimp"> в свою разметку, движок его не переносит.
|
|
105
|
+
* @param {string} canvasId
|
|
106
|
+
* @param {Object} size - { width, height } из конфига канвасов.
|
|
107
|
+
* @param {HTMLElement} [container] - Точка монтирования (по умолчанию body).
|
|
108
|
+
* @returns {HTMLCanvasElement}
|
|
109
|
+
*/
|
|
110
|
+
export function ensureCanvas(canvasId, size, container = document.body) {
|
|
111
|
+
const canvas =
|
|
112
|
+
document.getElementById(canvasId) ?? document.createElement('canvas');
|
|
113
|
+
|
|
114
|
+
if (!canvas.parentNode) {
|
|
115
|
+
canvas.setAttribute('id', canvasId);
|
|
116
|
+
canvas.width = size.width;
|
|
117
|
+
canvas.height = size.height;
|
|
118
|
+
container.appendChild(canvas);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return canvas;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Набор id, которые собирает каркас. Нужен тесту паритета с pug.
|
|
126
|
+
* @returns {Set<string>}
|
|
127
|
+
*/
|
|
128
|
+
export function shellIds() {
|
|
129
|
+
const ids = new Set();
|
|
130
|
+
const walk = nodes => {
|
|
131
|
+
for (const node of nodes) {
|
|
132
|
+
if (node.id) {
|
|
133
|
+
ids.add(node.id);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
walk(node.children ?? []);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
walk(SHELL);
|
|
141
|
+
|
|
142
|
+
return ids;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// рекурсивно достраивает узлы: элемент с таким id уже есть — берём его и
|
|
146
|
+
// спускаемся внутрь (каркас мог быть собран частично), иначе создаём.
|
|
147
|
+
// Безымянные узлы (декоративные p/a внутри #auth-link) достраиваются только
|
|
148
|
+
// вместе со своим родителем: по ним нельзя проверить, есть ли они уже, и в
|
|
149
|
+
// готовой разметке они бы удвоились
|
|
150
|
+
function buildNodes(nodes, parent, parentCreated) {
|
|
151
|
+
for (const node of nodes) {
|
|
152
|
+
if (!node.id) {
|
|
153
|
+
if (parentCreated) {
|
|
154
|
+
const elem = createNode(node);
|
|
155
|
+
|
|
156
|
+
parent.appendChild(elem);
|
|
157
|
+
buildNodes(node.children ?? [], elem, true);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const existing = document.getElementById(node.id);
|
|
164
|
+
const elem = existing ?? createNode(node);
|
|
165
|
+
|
|
166
|
+
if (!existing) {
|
|
167
|
+
parent.appendChild(elem);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
buildNodes(node.children ?? [], elem, !existing);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function createNode({ tag, id, attrs, text }) {
|
|
175
|
+
const elem = document.createElement(tag);
|
|
176
|
+
|
|
177
|
+
if (id) {
|
|
178
|
+
elem.setAttribute('id', id);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const [name, value] of Object.entries(attrs ?? {})) {
|
|
182
|
+
elem.setAttribute(name, value);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (text !== undefined) {
|
|
186
|
+
elem.textContent = text;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return elem;
|
|
190
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//- нейтральный каркас формы авторизации: заголовок и help-секции
|
|
2
|
+
//- заполняет AuthView из authSchema.texts игры (AUTH_DATA, порт 1)
|
|
3
|
+
div#auth
|
|
4
|
+
|
|
5
|
+
div#auth-form.card
|
|
6
|
+
|
|
7
|
+
h2#auth-title
|
|
8
|
+
|
|
9
|
+
div#auth-error
|
|
10
|
+
|
|
11
|
+
div#auth-fields
|
|
12
|
+
|
|
13
|
+
input#auth-enter(type='button', autofocus, value='Start')
|
|
14
|
+
|
|
15
|
+
div#auth-informs
|
|
16
|
+
|
|
17
|
+
div#auth-link
|
|
18
|
+
p
|
|
19
|
+
a(href='https://github.com/lgick/vimp') GitHub
|
|
20
|
+
p © #{new Date().getFullYear()} VIMP
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//- скрыт по умолчанию: открывается из main.js только после успешного логина
|
|
2
|
+
//- в #lobby-auth (Этап B2, LobbyAuthCtrl 'authenticated')
|
|
3
|
+
div#lobby(style='display:none')
|
|
4
|
+
div.lobby-grid
|
|
5
|
+
//- левая панель: профиль + форма создания комнаты (lobby-page-plan)
|
|
6
|
+
div#lobby-setup-panel.card
|
|
7
|
+
h2= 'VIMP'
|
|
8
|
+
//- бейдж входа (Этап B2): управляется LobbyAuthView, показывается
|
|
9
|
+
//- вместе с #lobby после успешной авторизации
|
|
10
|
+
div#lobby-user
|
|
11
|
+
div
|
|
12
|
+
span= 'Signed in as '
|
|
13
|
+
b#lobby-user-nick
|
|
14
|
+
input#lobby-user-logout(type='button', value='Sign out')
|
|
15
|
+
hr.lobby-divider
|
|
16
|
+
h3.lobby-section-title= 'HOST A GAME'
|
|
17
|
+
div#lobby-create
|
|
18
|
+
//- селектор игры (lobby-page-plan): рабочий с первой же игрой в
|
|
19
|
+
//- каталоге мастера, а не только когда их несколько
|
|
20
|
+
div.form-row
|
|
21
|
+
label.form-label(for='lobby-game')= 'Select Game'
|
|
22
|
+
select#lobby-game.field-select
|
|
23
|
+
div.form-row
|
|
24
|
+
label.form-label(for='lobby-name')= 'Server name'
|
|
25
|
+
input#lobby-name.field-text(type='text', maxlength='30')
|
|
26
|
+
//- поля настроек комнаты генерируются по явной схеме roomForm
|
|
27
|
+
//- манифеста активной игры (docs/en/plugin-api.md "Form schema")
|
|
28
|
+
div#lobby-fields
|
|
29
|
+
input#lobby-host(type='button', value='Create server')
|
|
30
|
+
//- правая панель: вкладки Active Servers / Leaderboard (lobby-page-plan)
|
|
31
|
+
div#lobby-browser-panel.card
|
|
32
|
+
//- абсолютный слой: высоту .card задаёт форма слева (stretch), а
|
|
33
|
+
//- список внутри скроллится, не раздувая карточку (lobby-page-plan)
|
|
34
|
+
div.lobby-browser-inner
|
|
35
|
+
div.lobby-tabs-header
|
|
36
|
+
input#btn-show-servers.lobby-tab-btn.active(type='button', value='Active Servers')
|
|
37
|
+
input#btn-show-leaderboard.lobby-tab-btn(type='button', value='Leaderboard')
|
|
38
|
+
div#lobby-servers-content
|
|
39
|
+
input#lobby-search.field-text(type='text', placeholder='Search servers…')
|
|
40
|
+
ul#lobby-list
|
|
41
|
+
p#lobby-empty= 'No servers available'
|
|
42
|
+
input#lobby-more(type='button', value='Load more')
|
|
43
|
+
div#lobby-leaderboard-content(style='display:none')
|
|
44
|
+
div.lobby-leaderboard-header
|
|
45
|
+
span#leaderboard-title
|
|
46
|
+
span#leaderboard-total
|
|
47
|
+
ol#lobby-leaderboard-list
|
|
48
|
+
p#lobby-my-placement
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//- логин-гейт лобби (Этап B2): показывается до #lobby, пока клиент не
|
|
2
|
+
//- авторизован через central auth-сервис (packages/auth). Кнопки провайдеров
|
|
3
|
+
//- фильтруются JS по authClient.providers; неактивные скрыты
|
|
4
|
+
div#lobby-auth
|
|
5
|
+
|
|
6
|
+
div#lobby-auth-login.card
|
|
7
|
+
h2= 'VIMP'
|
|
8
|
+
p= 'Sign in to play'
|
|
9
|
+
div#lobby-auth-login-buttons
|
|
10
|
+
input.lobby-auth-provider#lobby-auth-login-github(type='button', data-provider='github', value='Sign in with GitHub')
|
|
11
|
+
input.lobby-auth-provider#lobby-auth-login-google(type='button', data-provider='google', value='Sign in with Google')
|
|
12
|
+
input.lobby-auth-provider#lobby-auth-login-apple(type='button', data-provider='apple', value='Sign in with Apple')
|
|
13
|
+
div#lobby-auth-login-error
|
|
14
|
+
|
|
15
|
+
div#lobby-auth-nick.card(style='display:none')
|
|
16
|
+
h2= 'Choose your nick'
|
|
17
|
+
input#lobby-auth-nick-input.field-text(type='text', maxlength='14', placeholder='YourNick')
|
|
18
|
+
div#lobby-auth-nick-error
|
|
19
|
+
input#lobby-auth-nick-submit(type='button', value='Continue')
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// коды закрытия транспорта — общий контракт серверных контуров (мастер,
|
|
2
|
+
// dedicated, Worker-хост) и клиента, тот же приём, что в gameCodes.js:
|
|
3
|
+
// значение не должно расходиться между ними молча. Отказы политики — те, что
|
|
4
|
+
// клиент не лечит перезагрузкой, — перечислены в
|
|
5
|
+
// `client/network/policyClose.js`, и тест там требует явного решения по
|
|
6
|
+
// каждому коду из этой карты
|
|
7
|
+
export default {
|
|
8
|
+
staleHost: 4000, // master/SignalingServer.js — сигнальный WS хоста протух
|
|
9
|
+
invalidOrigin: 4001, // master/SignalingServer.js, dedicated/main.js
|
|
10
|
+
blocked: 4002, // master/SignalingServer.js — хостер заблокирован рейтингом
|
|
11
|
+
kickForMaxLatency: 4003, // host/HostGame.js
|
|
12
|
+
kickForMissedPings: 4004, // host/HostGame.js
|
|
13
|
+
kickIdle: 4005, // host/HostGame.js
|
|
14
|
+
roomFull: 4006, // host/PortMachine.js
|
|
15
|
+
// 4007 свободен
|
|
16
|
+
handshakeTimeout: 4008, // dedicated/main.js
|
|
17
|
+
tooManyConnections: 4009, // dedicated/main.js
|
|
18
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Env-переопределения серверного конфига (Этап 4 плана standalone-sdk).
|
|
2
|
+
// Раньше этот блок жил в master/main.js под условием NODE_ENV=production:
|
|
3
|
+
// лобби-мастер в dev работает на дефолтах конфига. Dedicated-серверу так
|
|
4
|
+
// нельзя — игру, порт и настройки комнаты он получает только из окружения,
|
|
5
|
+
// в том числе локально, — поэтому чтение вынесено сюда и применяется каждым
|
|
6
|
+
// входом по своим правилам.
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Применяет env-переопределения к конфигу мастера.
|
|
10
|
+
* @param {Object} config - Синглтон lib/config.js.
|
|
11
|
+
* @param {Object} [env] - Окружение (по умолчанию process.env).
|
|
12
|
+
*/
|
|
13
|
+
export function applyMasterEnv(config, env = process.env) {
|
|
14
|
+
if (env.VIMP_DOMAIN) {
|
|
15
|
+
config.set('master:domain', env.VIMP_DOMAIN);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// порт серверного процесса
|
|
19
|
+
if (env.VIMP_MASTER_PORT) {
|
|
20
|
+
config.set('master:port', Number(env.VIMP_MASTER_PORT));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// домен central auth-сервиса (Этап B2) — попадает в CSP connect-src, т.к.
|
|
24
|
+
// лобби делает туда прямой fetch (POST /nick)
|
|
25
|
+
if (env.VIMP_AUTH_SERVICE_URL) {
|
|
26
|
+
config.set('master:security:authServiceUrl', env.VIMP_AUTH_SERVICE_URL);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// список игр-плагинов мастера (Этап A2), по образцу остальных *_MATRIX
|
|
30
|
+
// env-переопределений — JSON-массив {id, package, version}
|
|
31
|
+
if (env.GAMES_MATRIX) {
|
|
32
|
+
config.set('master:games', JSON.parse(env.GAMES_MATRIX));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Настройки комнаты dedicated-сервера: VIMP_DEDICATED_ROOM — JSON-объект
|
|
38
|
+
* (map, maxPlayers, roundTime, mapTime, friendlyFire, seed). Мусор в
|
|
39
|
+
* переменной не должен ронять процесс молча — отказ именованный.
|
|
40
|
+
* @param {Object} [env] - Окружение (по умолчанию process.env).
|
|
41
|
+
* @returns {Object} Переопределения комнаты.
|
|
42
|
+
*/
|
|
43
|
+
export function readDedicatedRoom(env = process.env) {
|
|
44
|
+
if (!env.VIMP_DEDICATED_ROOM) {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let room;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
room = JSON.parse(env.VIMP_DEDICATED_ROOM);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
throw new Error(`VIMP_DEDICATED_ROOM: invalid JSON — ${e.message}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!room || typeof room !== 'object' || Array.isArray(room)) {
|
|
57
|
+
throw new Error('VIMP_DEDICATED_ROOM: expected a JSON object');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return room;
|
|
61
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHostRuntime } from '../lib/createHostRuntime.js';
|
|
2
|
+
import { offlinePlayerData } from '../lib/offlinePlayerData.js';
|
|
2
3
|
import VirtualClock, { flushMicrotasks } from './VirtualClock.js';
|
|
3
4
|
import RecordingSocketManager from './RecordingSocketManager.js';
|
|
4
5
|
import VirtualClient from './VirtualClient.js';
|
|
@@ -32,14 +33,8 @@ const RECORDED_PORTS = {
|
|
|
32
33
|
};
|
|
33
34
|
|
|
34
35
|
// В headless-контуре нет ни мастера, ни auth-сервиса — участник стартует с
|
|
35
|
-
// пустым
|
|
36
|
-
|
|
37
|
-
// Node просто не разрешается.
|
|
38
|
-
const emptyProfileFetch = async () => ({
|
|
39
|
-
ok: true,
|
|
40
|
-
status: 200,
|
|
41
|
-
json: async () => ({ rank: 0, state: null }),
|
|
42
|
-
});
|
|
36
|
+
// пустым профилем (lib/offlinePlayerData.js, общее со standalone/dedicated).
|
|
37
|
+
const emptyProfileFetch = offlinePlayerData();
|
|
43
38
|
|
|
44
39
|
/**
|
|
45
40
|
* Приводит сценарий к полной форме и проверяет обязательные поля.
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import { access, readFile } from 'node:fs/promises';
|
|
2
1
|
import path from 'node:path';
|
|
3
2
|
import { pathToFileURL } from 'node:url';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
assertGameConfigShape,
|
|
7
|
-
} from '../lib/gamePlugin.js';
|
|
3
|
+
import { assertGameConfigShape } from '../lib/gamePlugin.js';
|
|
4
|
+
import { loadGamePackage } from '../lib/loadGamePackage.js';
|
|
8
5
|
|
|
9
6
|
// Поиск игры для headless-прогона. В браузере плагин грузится по URL из
|
|
10
7
|
// GameManifest мастера; в Node URL-ов нет, поэтому источников три, по
|
|
@@ -56,64 +53,15 @@ export async function loadGameForSim({ game = null, core = null } = {}) {
|
|
|
56
53
|
return plugin;
|
|
57
54
|
}
|
|
58
55
|
|
|
56
|
+
// путь --game указывает либо на пакет игры, либо прямо на манифест; сама
|
|
57
|
+
// загрузка общая с dedicated-сервером (lib/loadGamePackage.js)
|
|
59
58
|
async function loadFromManifest(game, core) {
|
|
60
|
-
const
|
|
59
|
+
const target = game.endsWith('.json')
|
|
61
60
|
? path.resolve(game)
|
|
62
|
-
: path.resolve(game, 'dist
|
|
63
|
-
const
|
|
61
|
+
: path.resolve(game, 'dist');
|
|
62
|
+
const pkg = await loadGamePackage(target, { core });
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const baseDir = path.dirname(manifestPath);
|
|
68
|
-
const { assetsBase } = manifest;
|
|
69
|
-
const hostPlugin = await importDefault(
|
|
70
|
-
baseDir,
|
|
71
|
-
manifest.entries.host,
|
|
72
|
-
assetsBase,
|
|
73
|
-
);
|
|
74
|
-
const clientPlugin = await importDefault(
|
|
75
|
-
baseDir,
|
|
76
|
-
manifest.entries.client,
|
|
77
|
-
assetsBase,
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
assertPluginMatchesManifest(manifest, {
|
|
81
|
-
host: hostPlugin,
|
|
82
|
-
client: clientPlugin,
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const nodeCore = core ?? manifest.entries.wasmNode ?? null;
|
|
86
|
-
|
|
87
|
-
if (!nodeCore) {
|
|
88
|
-
throw new Error(
|
|
89
|
-
`${manifestPath}: no node build of the core — add entries.wasmNode ` +
|
|
90
|
-
`to the manifest (core/pkg-node) or pass --core <path>`,
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const corePath = path.resolve(baseDir, nodeCore);
|
|
95
|
-
|
|
96
|
-
// манифест объявляет поле, но опубликованный пакет мог не довезти файл
|
|
97
|
-
// (ignore-правила срезают каталог внутри files) — без этой проверки отказ
|
|
98
|
-
// приходит сырым ERR_MODULE_NOT_FOUND из резолвера
|
|
99
|
-
try {
|
|
100
|
-
await access(corePath);
|
|
101
|
-
} catch {
|
|
102
|
-
throw new Error(
|
|
103
|
-
`${manifestPath}: entries.wasmNode points at '${nodeCore}', but ` +
|
|
104
|
-
`${corePath} does not exist — the game package was published ` +
|
|
105
|
-
`without its node core (npm run core:build:node) or pass --core <path>`,
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
id: manifest.id,
|
|
111
|
-
manifest,
|
|
112
|
-
hostPlugin,
|
|
113
|
-
clientPlugin,
|
|
114
|
-
wasmUrl: pathToFileURL(path.resolve(baseDir, nodeCore)).href,
|
|
115
|
-
source: manifestPath,
|
|
116
|
-
};
|
|
64
|
+
return { ...pkg, source: pkg.manifestPath };
|
|
117
65
|
}
|
|
118
66
|
|
|
119
67
|
// фикстура даёт рабочий контур сразу, ещё до того как игра соберёт pkg-node:
|
|
@@ -133,36 +81,3 @@ async function loadFixture(core) {
|
|
|
133
81
|
source: FIXTURE_SOURCE,
|
|
134
82
|
};
|
|
135
83
|
}
|
|
136
|
-
|
|
137
|
-
// зеркало lib/gamePlugin.js:loadClientPlugin — манифест мог быть пересобран
|
|
138
|
-
// без dist/: прогон на старом плагине даёт зелёный вердикт о том, чего в
|
|
139
|
-
// сборке уже нет
|
|
140
|
-
function assertPluginMatchesManifest(manifest, plugins) {
|
|
141
|
-
for (const [half, plugin] of Object.entries(plugins)) {
|
|
142
|
-
if (plugin.engineApi !== manifest.engineApi) {
|
|
143
|
-
throw new Error(
|
|
144
|
-
`game "${manifest.id}": ${half} plugin engineApi ` +
|
|
145
|
-
`v${plugin.engineApi} does not match manifest engineApi ` +
|
|
146
|
-
`v${manifest.engineApi} — stale dist/`,
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function importDefault(baseDir, entry, assetsBase) {
|
|
153
|
-
const file = path.resolve(baseDir, stripBase(entry, assetsBase));
|
|
154
|
-
|
|
155
|
-
return import(pathToFileURL(file).href).then(module => module.default);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// entries.host/client — URL-ы, какими их видит браузер (`assetsBase` + путь
|
|
159
|
-
// внутри пакета). На диске этой базе соответствует каталог манифеста,
|
|
160
|
-
// поэтому абсолютный URL сначала обрезается до пути внутри пакета — иначе
|
|
161
|
-
// path.resolve увёл бы в корень файловой системы.
|
|
162
|
-
function stripBase(entry, assetsBase) {
|
|
163
|
-
if (assetsBase && entry.startsWith(assetsBase)) {
|
|
164
|
-
return entry.slice(assetsBase.length);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return entry.replace(/^\/+/, '');
|
|
168
|
-
}
|
package/src/host/HostGame.js
CHANGED
|
@@ -11,6 +11,7 @@ import VoteCoordinator from './meta/core/VoteCoordinator.js';
|
|
|
11
11
|
import RoundManager from './meta/core/RoundManager.js';
|
|
12
12
|
import CommandProcessor from './meta/core/CommandProcessor.js';
|
|
13
13
|
import { sanitizeMessage } from '../lib/sanitizers.js';
|
|
14
|
+
import closeCodes from '../config/closeCodes.js';
|
|
14
15
|
import clock from '../lib/clock.js';
|
|
15
16
|
import GameCoreAdapter from './GameCoreAdapter.js';
|
|
16
17
|
import DebugRecorder from './DebugRecorder.js';
|
|
@@ -279,7 +280,11 @@ export default class HostGame {
|
|
|
279
280
|
|
|
280
281
|
if (user && !this._isHostPlayer(user)) {
|
|
281
282
|
console.warn(`[RTT] Kick ${user.name} — pong latency exceeded`);
|
|
282
|
-
this._socketManager.close(
|
|
283
|
+
this._socketManager.close(
|
|
284
|
+
user.socketId,
|
|
285
|
+
closeCodes.kickForMaxLatency,
|
|
286
|
+
'kickForMaxLatency',
|
|
287
|
+
);
|
|
283
288
|
this.removeUser(gameId);
|
|
284
289
|
}
|
|
285
290
|
}
|
|
@@ -290,7 +295,11 @@ export default class HostGame {
|
|
|
290
295
|
|
|
291
296
|
if (user && !this._isHostPlayer(user)) {
|
|
292
297
|
console.warn(`[RTT] Kick ${user.name} — no response to pings`);
|
|
293
|
-
this._socketManager.close(
|
|
298
|
+
this._socketManager.close(
|
|
299
|
+
user.socketId,
|
|
300
|
+
closeCodes.kickForMissedPings,
|
|
301
|
+
'kickForMissedPings',
|
|
302
|
+
);
|
|
294
303
|
this.removeUser(gameId);
|
|
295
304
|
}
|
|
296
305
|
}
|
|
@@ -439,7 +448,7 @@ export default class HostGame {
|
|
|
439
448
|
}
|
|
440
449
|
|
|
441
450
|
usersToKick.forEach(user => {
|
|
442
|
-
this._socketManager.close(user.socketId,
|
|
451
|
+
this._socketManager.close(user.socketId, closeCodes.kickIdle, 'kickIdle');
|
|
443
452
|
this.removeUser(user.gameId);
|
|
444
453
|
});
|
|
445
454
|
}
|
|
@@ -599,6 +608,35 @@ export default class HostGame {
|
|
|
599
608
|
}
|
|
600
609
|
}
|
|
601
610
|
|
|
611
|
+
/**
|
|
612
|
+
* Публичный teardown матча: останов таймеров, финальная синхронизация
|
|
613
|
+
* профилей и снятие всех участников. Во вкладке матч умирает вместе с
|
|
614
|
+
* Worker'ом, а долгоживущему процессу (dedicated-сервер) нужен graceful
|
|
615
|
+
* shutdown — иначе таймеры держат процесс, а rank/state теряются.
|
|
616
|
+
* @returns {Promise} Завершение финальной синхронизации профилей.
|
|
617
|
+
*/
|
|
618
|
+
async destroy() {
|
|
619
|
+
this._timerManager.stopGameTimers();
|
|
620
|
+
this._timerManager.stopIdleCheckTimer();
|
|
621
|
+
this._timerManager.stopAllVoteTimers();
|
|
622
|
+
this._timerManager.stopAllBlockedVoteTimers();
|
|
623
|
+
|
|
624
|
+
// flushAll до снятия участников: removeUser чистит запись PlayerDataSync,
|
|
625
|
+
// и после него синхронизировать было бы уже нечего. Ждём здесь же —
|
|
626
|
+
// иначе removeUser стартует второй flush с той же накопленной дельтой
|
|
627
|
+
// (двойной зачёт рейтинга), а destroy разрешился бы раньше, чем эти
|
|
628
|
+
// запросы уйдут
|
|
629
|
+
await this._playerDataSync.flushAll();
|
|
630
|
+
|
|
631
|
+
// getAll() отдаёт копию — снятие внутри цикла реестр не ломает
|
|
632
|
+
for (const user of this._participants.getAll()) {
|
|
633
|
+
// запись уже синхронизирована — финальный flush внутри removeUser
|
|
634
|
+
// не нужен и был бы повтором
|
|
635
|
+
this._playerDataSync.removeUser(user.gameId);
|
|
636
|
+
this.removeUser(user.gameId);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
602
640
|
// ***** эстафета Worker'ов (Этап 5.2) ***** //
|
|
603
641
|
|
|
604
642
|
// запрашивает перенос: на ближайшей границе раунда игра останавливается
|