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,294 @@
|
|
|
1
|
+
// Хендшейк-машина клиента, вынутая из host.worker.js: автомат по клиентским
|
|
2
|
+
// портам 0–8 (см. docs/en/network.md). Изоморфна — ни self, ни postMessage,
|
|
3
|
+
// ни DOM: всё, что знает про транспорт, приходит через makeSocket. Поэтому её
|
|
4
|
+
// крутит и Worker браузерного хоста, и inline-хост standalone SDK, и
|
|
5
|
+
// Node-процесс dedicated-сервера: копий автомата быть не должно, иначе они
|
|
6
|
+
// разъедутся ровно так, как разъезжались бы копии createHostRuntime.
|
|
7
|
+
|
|
8
|
+
import wsports from '../config/wsports.js';
|
|
9
|
+
import closeCodes from '../config/closeCodes.js';
|
|
10
|
+
import { validateAuth } from '../lib/validators.js';
|
|
11
|
+
|
|
12
|
+
// PC (client ports): порты получения данных от клиента
|
|
13
|
+
const PC_CONFIG_READY = wsports.client.CONFIG_READY;
|
|
14
|
+
const PC_AUTH_RESPONSE = wsports.client.AUTH_RESPONSE;
|
|
15
|
+
const PC_MODULES_READY = wsports.client.MODULES_READY;
|
|
16
|
+
const PC_MAP_READY = wsports.client.MAP_READY;
|
|
17
|
+
const PC_FIRST_SHOT_READY = wsports.client.FIRST_SHOT_READY;
|
|
18
|
+
const PC_KEYS_DATA = wsports.client.KEYS_DATA;
|
|
19
|
+
const PC_CHAT_DATA = wsports.client.CHAT_DATA;
|
|
20
|
+
const PC_VOTE_DATA = wsports.client.VOTE_DATA;
|
|
21
|
+
const PC_PONG = wsports.client.PONG;
|
|
22
|
+
|
|
23
|
+
const PORT_COUNT = 9;
|
|
24
|
+
|
|
25
|
+
export default class PortMachine {
|
|
26
|
+
/**
|
|
27
|
+
* @param {Object} deps
|
|
28
|
+
* @param {Object} deps.host - HostGame.
|
|
29
|
+
* @param {Object} deps.socketManager - транспорт (per-user send/close).
|
|
30
|
+
* @param {Object} deps.clientCfg - конфиг клиента (порт 0).
|
|
31
|
+
* @param {Object} deps.authSchema - hostPlugin.authSchema.
|
|
32
|
+
* @param {Function} deps.makeSocket - (socketId) => { send, sendBinary, close }.
|
|
33
|
+
* @param {Object} deps.identity - стратегия идентичности (./identity.js).
|
|
34
|
+
*/
|
|
35
|
+
constructor({ host, socketManager, clientCfg, authSchema, makeSocket, identity }) {
|
|
36
|
+
this._host = host;
|
|
37
|
+
this._socketManager = socketManager;
|
|
38
|
+
this._clientCfg = clientCfg;
|
|
39
|
+
this._authSchema = authSchema;
|
|
40
|
+
this._makeSocket = makeSocket;
|
|
41
|
+
this._identity = identity;
|
|
42
|
+
|
|
43
|
+
// поля формы = поля стратегии идентичности + игровые. Один и тот же
|
|
44
|
+
// список уходит клиенту (порт 0) и валидируется на порту 1: гостевой ник
|
|
45
|
+
// доезжает до формы ровно тем же каналом, что игровые поля. Идентичность
|
|
46
|
+
// идёт первой — ник это первое, что заполняет игрок
|
|
47
|
+
this._authParams = [
|
|
48
|
+
...(identity.params ?? []),
|
|
49
|
+
...(authSchema.params ?? []),
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
// состояние подключений: socketId → { gameId, methods, enabled }
|
|
53
|
+
this._clients = new Map();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Новое подключение клиента: регистрация сокета и старт хендшейка.
|
|
58
|
+
* @param {string} socketId
|
|
59
|
+
*/
|
|
60
|
+
connect(socketId) {
|
|
61
|
+
if (this._clients.has(socketId)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this._socketManager.addUser(socketId, this._makeSocket(socketId));
|
|
66
|
+
|
|
67
|
+
// комната заполнена (люди + боты) — отказ без порт-машины
|
|
68
|
+
// (очереди ожидания легаси-сервера в P2P-комнате нет)
|
|
69
|
+
if (this._host.isFull) {
|
|
70
|
+
// причину доставит close (TECH_INFORM перед close_client)
|
|
71
|
+
this._socketManager.close(socketId, closeCodes.roomFull, 'roomFull', [
|
|
72
|
+
this._host.maxPlayers,
|
|
73
|
+
]);
|
|
74
|
+
this._socketManager.removeUser(socketId);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const state = {
|
|
79
|
+
gameId: undefined,
|
|
80
|
+
enabled: new Array(PORT_COUNT).fill(false),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
this._clients.set(socketId, state);
|
|
84
|
+
state.methods = this._buildPortMethods(socketId, state);
|
|
85
|
+
|
|
86
|
+
state.enabled[PC_CONFIG_READY] = true;
|
|
87
|
+
this._socketManager.sendConfig(socketId, this._clientCfg);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Подключение участника, уже восстановленного в HostGame (эстафета
|
|
92
|
+
* Worker'ов, Этап 5.2): порт-машина поднимается сразу в игровом
|
|
93
|
+
* состоянии, хендшейк не повторяется.
|
|
94
|
+
* @param {string} socketId
|
|
95
|
+
* @param {number} gameId
|
|
96
|
+
*/
|
|
97
|
+
restore(socketId, gameId) {
|
|
98
|
+
if (this._clients.has(socketId)) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this._socketManager.addUser(socketId, this._makeSocket(socketId));
|
|
103
|
+
|
|
104
|
+
const state = {
|
|
105
|
+
gameId,
|
|
106
|
+
enabled: new Array(PORT_COUNT).fill(false),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
state.methods = this._buildPortMethods(socketId, state);
|
|
110
|
+
state.enabled[PC_MAP_READY] = true;
|
|
111
|
+
state.enabled[PC_FIRST_SHOT_READY] = true;
|
|
112
|
+
state.enabled[PC_KEYS_DATA] = true;
|
|
113
|
+
state.enabled[PC_CHAT_DATA] = true;
|
|
114
|
+
state.enabled[PC_VOTE_DATA] = true;
|
|
115
|
+
state.enabled[PC_PONG] = true;
|
|
116
|
+
|
|
117
|
+
this._clients.set(socketId, state);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Входящее сообщение клиента (wire-кадр [port, payload] строкой).
|
|
122
|
+
* @param {string} socketId
|
|
123
|
+
* @param {string} data
|
|
124
|
+
*/
|
|
125
|
+
message(socketId, data) {
|
|
126
|
+
const state = this._clients.get(socketId);
|
|
127
|
+
|
|
128
|
+
if (!state) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let msg;
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
msg = JSON.parse(data);
|
|
136
|
+
} catch (e) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (msg && state.enabled[msg[0]]) {
|
|
141
|
+
state.methods[msg[0]](msg[1]);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Отключение клиента: снятие участника и чистка транспорта.
|
|
147
|
+
* @param {string} socketId
|
|
148
|
+
*/
|
|
149
|
+
disconnect(socketId) {
|
|
150
|
+
const state = this._clients.get(socketId);
|
|
151
|
+
|
|
152
|
+
if (!state) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
this._socketManager.removeUser(socketId);
|
|
157
|
+
|
|
158
|
+
if (state.gameId !== undefined) {
|
|
159
|
+
this._host.removeUser(state.gameId);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
this._clients.delete(socketId);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @param {string} socketId
|
|
167
|
+
* @returns {boolean} Есть ли порт-машина у этого соединения.
|
|
168
|
+
*/
|
|
169
|
+
has(socketId) {
|
|
170
|
+
return this._clients.has(socketId);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @param {string} socketId
|
|
175
|
+
* @returns {boolean} Дошёл ли клиент до созданного участника матча.
|
|
176
|
+
*/
|
|
177
|
+
hasParticipant(socketId) {
|
|
178
|
+
return this._clients.get(socketId)?.gameId !== undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* @returns {Iterator<string>} socketId живых соединений (для
|
|
183
|
+
* host.completeHandoff(new Set(...))).
|
|
184
|
+
*/
|
|
185
|
+
get socketIds() {
|
|
186
|
+
return this._clients.keys();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// порт-обработчики клиента (замыкание над gameId через state)
|
|
190
|
+
_buildPortMethods(socketId, state) {
|
|
191
|
+
const host = this._host;
|
|
192
|
+
const socketManager = this._socketManager;
|
|
193
|
+
|
|
194
|
+
return [
|
|
195
|
+
// 0: config ready
|
|
196
|
+
() => {
|
|
197
|
+
state.enabled[PC_AUTH_RESPONSE] = true;
|
|
198
|
+
|
|
199
|
+
// validators — код, по проводу не передаётся (форма клиента берёт
|
|
200
|
+
// игровые валидаторы из своего бандла); texts — заголовок и
|
|
201
|
+
// help-секции игры для нейтрального каркаса auth.pug
|
|
202
|
+
socketManager.sendAuthData(socketId, {
|
|
203
|
+
elems: this._authSchema.elems,
|
|
204
|
+
params: this._authParams,
|
|
205
|
+
texts: this._authSchema.texts,
|
|
206
|
+
});
|
|
207
|
+
state.enabled[PC_CONFIG_READY] = false;
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
// 1: auth response. Ник — не свободный ввод игровой формы, а результат
|
|
211
|
+
// стратегии идентичности (claim проверенного токена в лобби, поле
|
|
212
|
+
// формы в гостевом контуре)
|
|
213
|
+
data => {
|
|
214
|
+
if (!data || typeof data !== 'object') {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const err = validateAuth(
|
|
219
|
+
data,
|
|
220
|
+
this._authParams,
|
|
221
|
+
this._authSchema.validators,
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
if (err) {
|
|
225
|
+
socketManager.sendAuthResult(socketId, err);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
this._identity
|
|
230
|
+
.resolve(data, socketId)
|
|
231
|
+
.then(name => {
|
|
232
|
+
// клиент мог отключиться, пока проверялась личность
|
|
233
|
+
if (!this._clients.has(socketId)) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
state.enabled[PC_AUTH_RESPONSE] = false;
|
|
238
|
+
state.enabled[PC_MODULES_READY] = true;
|
|
239
|
+
|
|
240
|
+
host.createUser({ ...data, name }, socketId, createdId => {
|
|
241
|
+
state.gameId = createdId;
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
socketManager.sendTechInform(socketId, 'loading');
|
|
245
|
+
socketManager.sendAuthResult(socketId, undefined);
|
|
246
|
+
})
|
|
247
|
+
.catch(() => {
|
|
248
|
+
socketManager.sendAuthResult(socketId, [
|
|
249
|
+
{ name: this._identity.errorField ?? 'token', error: 'invalid' },
|
|
250
|
+
]);
|
|
251
|
+
});
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
// 2: modules ready
|
|
255
|
+
() => {
|
|
256
|
+
state.enabled[PC_MODULES_READY] = false;
|
|
257
|
+
state.enabled[PC_MAP_READY] = true;
|
|
258
|
+
state.enabled[PC_FIRST_SHOT_READY] = true;
|
|
259
|
+
state.enabled[PC_KEYS_DATA] = true;
|
|
260
|
+
state.enabled[PC_CHAT_DATA] = true;
|
|
261
|
+
state.enabled[PC_VOTE_DATA] = true;
|
|
262
|
+
state.enabled[PC_PONG] = true;
|
|
263
|
+
|
|
264
|
+
host.sendMap(state.gameId);
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
// 3: map ready
|
|
268
|
+
() => host.mapReady(state.gameId),
|
|
269
|
+
|
|
270
|
+
// 4: first shot ready
|
|
271
|
+
() => host.firstShotReady(state.gameId),
|
|
272
|
+
|
|
273
|
+
// 5: keys data ('seq:action:name')
|
|
274
|
+
keyEventString => {
|
|
275
|
+
if (typeof keyEventString === 'string') {
|
|
276
|
+
host.updateKeys(state.gameId, keyEventString);
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
// 6: chat data
|
|
281
|
+
message => host.pushMessage(state.gameId, message),
|
|
282
|
+
|
|
283
|
+
// 7: vote data
|
|
284
|
+
data => {
|
|
285
|
+
if (data) {
|
|
286
|
+
host.parseVote(state.gameId, data);
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
// 8: pong
|
|
291
|
+
pingId => host.updateRTT(state.gameId, pingId),
|
|
292
|
+
];
|
|
293
|
+
}
|
|
294
|
+
}
|
package/src/host/host.worker.js
CHANGED
|
@@ -5,79 +5,28 @@
|
|
|
5
5
|
// троттлятся в фоновой вкладке). RTCPeerConnection живут в главном потоке —
|
|
6
6
|
// сюда приходят уже разобранные пакеты клиентов, обратно уходят wire-кадры
|
|
7
7
|
// (JSON-строки и бинарные ArrayBuffer'ы через Transferable).
|
|
8
|
+
//
|
|
9
|
+
// Сам хендшейк клиента (порты 0–8) живёт в изоморфной ./PortMachine.js —
|
|
10
|
+
// этот файл только адаптер: postMessage-транспорт, лобби-стратегия
|
|
11
|
+
// идентичности и свитч сообщений главного потока.
|
|
8
12
|
|
|
9
13
|
import authClientConfig from '../config/authClient.js';
|
|
10
14
|
import lobbyConfig from '../config/lobby.js';
|
|
11
15
|
import wsports from '../config/wsports.js';
|
|
12
16
|
import { createHostRuntime } from '../lib/createHostRuntime.js';
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
// PC (client ports): порты получения данных от клиента
|
|
17
|
-
const PC_CONFIG_READY = wsports.client.CONFIG_READY;
|
|
18
|
-
const PC_AUTH_RESPONSE = wsports.client.AUTH_RESPONSE;
|
|
19
|
-
const PC_MODULES_READY = wsports.client.MODULES_READY;
|
|
20
|
-
const PC_MAP_READY = wsports.client.MAP_READY;
|
|
21
|
-
const PC_FIRST_SHOT_READY = wsports.client.FIRST_SHOT_READY;
|
|
22
|
-
const PC_KEYS_DATA = wsports.client.KEYS_DATA;
|
|
23
|
-
const PC_CHAT_DATA = wsports.client.CHAT_DATA;
|
|
24
|
-
const PC_VOTE_DATA = wsports.client.VOTE_DATA;
|
|
25
|
-
const PC_PONG = wsports.client.PONG;
|
|
17
|
+
import PortMachine from './PortMachine.js';
|
|
18
|
+
import { createTokenIdentity } from './identity.js';
|
|
26
19
|
|
|
27
20
|
// PS (server ports): порты отправки данных клиенту
|
|
28
21
|
const PS_TECH_INFORM_DATA = wsports.server.TECH_INFORM_DATA;
|
|
29
22
|
|
|
30
23
|
let host = null;
|
|
31
|
-
let
|
|
32
|
-
let clientCfg = null;
|
|
33
|
-
|
|
34
|
-
// HostPlugin игры (Этап 6.4): грузится динамически по entries.host из
|
|
35
|
-
// GameManifest (room.game) — до onInit движок игру не знает вовсе
|
|
36
|
-
let hostPlugin = null;
|
|
37
|
-
|
|
38
|
-
// состояние подключений: socketId → { gameId, methods, enabled }
|
|
39
|
-
const clients = new Map();
|
|
24
|
+
let portMachine = null;
|
|
40
25
|
|
|
41
26
|
// эстафета Worker'ов (Этап 5.2): socketId → gameId участников, восстановленных
|
|
42
27
|
// из handoff-меты — их порт-машины поднимаются минуя хендшейк
|
|
43
28
|
let handoffClients = null;
|
|
44
29
|
|
|
45
|
-
// JWKS central auth-сервиса, проксированный мастером (Этап B3), закэширован
|
|
46
|
-
// на время жизни Worker'а — ключ меняется только при ротации
|
|
47
|
-
let jwksPromise = null;
|
|
48
|
-
|
|
49
|
-
function getJwks() {
|
|
50
|
-
if (!jwksPromise) {
|
|
51
|
-
jwksPromise = fetch(lobbyConfig.auth.jwksUrl).then(res => {
|
|
52
|
-
if (!res.ok) {
|
|
53
|
-
throw new Error(`jwks: HTTP ${res.status}`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return res.json();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// сбой не должен закэшироваться навсегда — следующий вход попробует снова
|
|
60
|
-
jwksPromise.catch(() => {
|
|
61
|
-
jwksPromise = null;
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return jwksPromise;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// проверяет identity-токен клиента (Этап B3): подпись RS256 по JWKS мастера,
|
|
69
|
-
// issuer, срок годности — возвращает проверенный ник или бросает исключение.
|
|
70
|
-
// Свободный ввод имени в игре больше не источник ника: он берётся из claim
|
|
71
|
-
async function verifyClientToken(token) {
|
|
72
|
-
const jwks = await getJwks();
|
|
73
|
-
const payload = await verifyIdentityToken(token, {
|
|
74
|
-
jwks,
|
|
75
|
-
issuer: authClientConfig.issuer,
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
return payload.nick;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
30
|
// wire-сокет пользователя: пишет кадры в главный поток (роутер WebRTC/loopback)
|
|
82
31
|
function makeWorkerSocket(socketId) {
|
|
83
32
|
return {
|
|
@@ -133,11 +82,22 @@ async function onInit(room, handoff = null) {
|
|
|
133
82
|
},
|
|
134
83
|
});
|
|
135
84
|
|
|
136
|
-
hostPlugin = runtime.hostPlugin;
|
|
137
|
-
clientCfg = runtime.clientCfg;
|
|
138
|
-
socketManager = runtime.socketManager;
|
|
139
85
|
host = runtime.host;
|
|
140
86
|
|
|
87
|
+
// в лобби личность игрока — claim identity-токена, проверенного по JWKS
|
|
88
|
+
// мастера (Этап B3); свободного ввода имени в форме игры нет
|
|
89
|
+
portMachine = new PortMachine({
|
|
90
|
+
host,
|
|
91
|
+
socketManager: runtime.socketManager,
|
|
92
|
+
clientCfg: runtime.clientCfg,
|
|
93
|
+
authSchema: runtime.hostPlugin.authSchema,
|
|
94
|
+
makeSocket: makeWorkerSocket,
|
|
95
|
+
identity: createTokenIdentity({
|
|
96
|
+
jwksUrl: lobbyConfig.auth.jwksUrl,
|
|
97
|
+
issuer: authClientConfig.issuer,
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
|
|
141
101
|
const seed = runtime.seed;
|
|
142
102
|
|
|
143
103
|
if (handoff) {
|
|
@@ -148,199 +108,22 @@ async function onInit(room, handoff = null) {
|
|
|
148
108
|
self.postMessage({ type: 'ready', mapName: host.currentMap, seed });
|
|
149
109
|
}
|
|
150
110
|
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
return [
|
|
154
|
-
// 0: config ready
|
|
155
|
-
() => {
|
|
156
|
-
state.enabled[PC_AUTH_RESPONSE] = true;
|
|
157
|
-
|
|
158
|
-
// validators — код, по проводу не передаётся (форма клиента берёт
|
|
159
|
-
// игровые валидаторы из своего бандла); texts — заголовок и
|
|
160
|
-
// help-секции игры для нейтрального каркаса auth.pug
|
|
161
|
-
socketManager.sendAuthData(socketId, {
|
|
162
|
-
elems: hostPlugin.authSchema.elems,
|
|
163
|
-
params: hostPlugin.authSchema.params,
|
|
164
|
-
texts: hostPlugin.authSchema.texts,
|
|
165
|
-
});
|
|
166
|
-
state.enabled[PC_CONFIG_READY] = false;
|
|
167
|
-
},
|
|
168
|
-
|
|
169
|
-
// 1: auth response. Ник — не свободный ввод игровой формы, а claim
|
|
170
|
-
// проверенного identity-токена (Этап B3); authSchema.params больше не
|
|
171
|
-
// содержит 'name', только игро-специфичные поля (например 'model')
|
|
172
|
-
data => {
|
|
173
|
-
if (!data || typeof data !== 'object') {
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const err = validateAuth(
|
|
178
|
-
data,
|
|
179
|
-
hostPlugin.authSchema.params,
|
|
180
|
-
hostPlugin.authSchema.validators,
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
if (err) {
|
|
184
|
-
socketManager.sendAuthResult(socketId, err);
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
verifyClientToken(data.token)
|
|
189
|
-
.then(name => {
|
|
190
|
-
// клиент мог отключиться, пока токен проверялся
|
|
191
|
-
if (!clients.has(socketId)) {
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
state.enabled[PC_AUTH_RESPONSE] = false;
|
|
196
|
-
state.enabled[PC_MODULES_READY] = true;
|
|
197
|
-
|
|
198
|
-
host.createUser({ ...data, name }, socketId, createdId => {
|
|
199
|
-
state.gameId = createdId;
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
socketManager.sendTechInform(socketId, 'loading');
|
|
203
|
-
socketManager.sendAuthResult(socketId, undefined);
|
|
204
|
-
})
|
|
205
|
-
.catch(() => {
|
|
206
|
-
socketManager.sendAuthResult(socketId, [
|
|
207
|
-
{ name: 'token', error: 'invalid' },
|
|
208
|
-
]);
|
|
209
|
-
});
|
|
210
|
-
},
|
|
211
|
-
|
|
212
|
-
// 2: modules ready
|
|
213
|
-
() => {
|
|
214
|
-
state.enabled[PC_MODULES_READY] = false;
|
|
215
|
-
state.enabled[PC_MAP_READY] = true;
|
|
216
|
-
state.enabled[PC_FIRST_SHOT_READY] = true;
|
|
217
|
-
state.enabled[PC_KEYS_DATA] = true;
|
|
218
|
-
state.enabled[PC_CHAT_DATA] = true;
|
|
219
|
-
state.enabled[PC_VOTE_DATA] = true;
|
|
220
|
-
state.enabled[PC_PONG] = true;
|
|
221
|
-
|
|
222
|
-
host.sendMap(state.gameId);
|
|
223
|
-
},
|
|
224
|
-
|
|
225
|
-
// 3: map ready
|
|
226
|
-
() => host.mapReady(state.gameId),
|
|
227
|
-
|
|
228
|
-
// 4: first shot ready
|
|
229
|
-
() => host.firstShotReady(state.gameId),
|
|
230
|
-
|
|
231
|
-
// 5: keys data ('seq:action:name')
|
|
232
|
-
keyEventString => {
|
|
233
|
-
if (typeof keyEventString === 'string') {
|
|
234
|
-
host.updateKeys(state.gameId, keyEventString);
|
|
235
|
-
}
|
|
236
|
-
},
|
|
237
|
-
|
|
238
|
-
// 6: chat data
|
|
239
|
-
message => host.pushMessage(state.gameId, message),
|
|
240
|
-
|
|
241
|
-
// 7: vote data
|
|
242
|
-
data => {
|
|
243
|
-
if (data) {
|
|
244
|
-
host.parseVote(state.gameId, data);
|
|
245
|
-
}
|
|
246
|
-
},
|
|
247
|
-
|
|
248
|
-
// 8: pong
|
|
249
|
-
pingId => host.updateRTT(state.gameId, pingId),
|
|
250
|
-
];
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// новое подключение клиента: порт-машина как в socket/index.js
|
|
111
|
+
// новое подключение клиента: участник из handoff-меты уже восстановлен в
|
|
112
|
+
// HostGame — его порт-машина поднимается сразу в игровом состоянии
|
|
254
113
|
function onConnect(socketId) {
|
|
255
|
-
if (!
|
|
114
|
+
if (!portMachine) {
|
|
256
115
|
return;
|
|
257
116
|
}
|
|
258
117
|
|
|
259
|
-
const socket = makeWorkerSocket(socketId);
|
|
260
|
-
|
|
261
|
-
socketManager.addUser(socketId, socket);
|
|
262
|
-
|
|
263
|
-
// эстафета (Этап 5.2): участник уже восстановлен в HostGame — порт-машина
|
|
264
|
-
// поднимается сразу в игровом состоянии, хендшейк не повторяется
|
|
265
118
|
const restoredGameId = handoffClients?.get(socketId);
|
|
266
119
|
|
|
267
120
|
if (restoredGameId !== undefined) {
|
|
268
121
|
handoffClients.delete(socketId);
|
|
269
|
-
|
|
270
|
-
const state = {
|
|
271
|
-
gameId: restoredGameId,
|
|
272
|
-
enabled: new Array(9).fill(false),
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
state.methods = buildPortMethods(socketId, state);
|
|
276
|
-
state.enabled[PC_MAP_READY] = true;
|
|
277
|
-
state.enabled[PC_FIRST_SHOT_READY] = true;
|
|
278
|
-
state.enabled[PC_KEYS_DATA] = true;
|
|
279
|
-
state.enabled[PC_CHAT_DATA] = true;
|
|
280
|
-
state.enabled[PC_VOTE_DATA] = true;
|
|
281
|
-
state.enabled[PC_PONG] = true;
|
|
282
|
-
|
|
283
|
-
clients.set(socketId, state);
|
|
122
|
+
portMachine.restore(socketId, restoredGameId);
|
|
284
123
|
return;
|
|
285
124
|
}
|
|
286
125
|
|
|
287
|
-
|
|
288
|
-
// (очереди ожидания легаси-сервера в P2P-комнате нет)
|
|
289
|
-
if (host.isFull) {
|
|
290
|
-
// причину доставит close (TECH_INFORM перед close_client)
|
|
291
|
-
socketManager.close(socketId, 4006, 'roomFull', [host.maxPlayers]);
|
|
292
|
-
socketManager.removeUser(socketId);
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const state = {
|
|
297
|
-
gameId: undefined,
|
|
298
|
-
enabled: new Array(9).fill(false),
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
clients.set(socketId, state);
|
|
302
|
-
state.methods = buildPortMethods(socketId, state);
|
|
303
|
-
|
|
304
|
-
state.enabled[PC_CONFIG_READY] = true;
|
|
305
|
-
socketManager.sendConfig(socketId, clientCfg);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// входящее сообщение клиента (wire-строка [port, payload])
|
|
309
|
-
function onClientMessage(socketId, data) {
|
|
310
|
-
const state = clients.get(socketId);
|
|
311
|
-
|
|
312
|
-
if (!state) {
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
let msg;
|
|
317
|
-
|
|
318
|
-
try {
|
|
319
|
-
msg = JSON.parse(data);
|
|
320
|
-
} catch (e) {
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (msg && state.enabled[msg[0]]) {
|
|
325
|
-
state.methods[msg[0]](msg[1]);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// отключение клиента
|
|
330
|
-
function onDisconnect(socketId) {
|
|
331
|
-
const state = clients.get(socketId);
|
|
332
|
-
|
|
333
|
-
if (!state) {
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
socketManager.removeUser(socketId);
|
|
338
|
-
|
|
339
|
-
if (state.gameId !== undefined) {
|
|
340
|
-
host.removeUser(state.gameId);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
clients.delete(socketId);
|
|
126
|
+
portMachine.connect(socketId);
|
|
344
127
|
}
|
|
345
128
|
|
|
346
129
|
// отладочные действия хоста (этап 6): запись живого матча в формат сценария
|
|
@@ -390,11 +173,11 @@ self.onmessage = async event => {
|
|
|
390
173
|
break;
|
|
391
174
|
|
|
392
175
|
case 'message':
|
|
393
|
-
|
|
176
|
+
portMachine?.message(msg.socketId, msg.data);
|
|
394
177
|
break;
|
|
395
178
|
|
|
396
179
|
case 'disconnect':
|
|
397
|
-
|
|
180
|
+
portMachine?.disconnect(msg.socketId);
|
|
398
181
|
break;
|
|
399
182
|
|
|
400
183
|
case 'update_maps':
|
|
@@ -431,7 +214,7 @@ self.onmessage = async event => {
|
|
|
431
214
|
// клиенты переподключены главным потоком — завершить перенос
|
|
432
215
|
case 'handoff_complete':
|
|
433
216
|
handoffClients = null;
|
|
434
|
-
host?.completeHandoff(new Set(
|
|
217
|
+
host?.completeHandoff(new Set(portMachine ? portMachine.socketIds : []));
|
|
435
218
|
break;
|
|
436
219
|
}
|
|
437
220
|
};
|