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.
Files changed (74) hide show
  1. package/package.json +11 -4
  2. package/src/client/InputListener.js +36 -0
  3. package/src/client/SoundManager.js +498 -0
  4. package/src/client/boot.js +113 -0
  5. package/src/client/components/controller/Auth.js +44 -0
  6. package/src/client/components/controller/CanvasManager.js +26 -0
  7. package/src/client/components/controller/Chat.js +56 -0
  8. package/src/client/components/controller/Controls.js +51 -0
  9. package/src/client/components/controller/Game.js +37 -0
  10. package/src/client/components/controller/Lobby.js +81 -0
  11. package/src/client/components/controller/LobbyAuth.js +41 -0
  12. package/src/client/components/controller/Panel.js +23 -0
  13. package/src/client/components/controller/Stat.js +33 -0
  14. package/src/client/components/controller/Vote.js +55 -0
  15. package/src/client/components/model/Auth.js +92 -0
  16. package/src/client/components/model/CanvasManager.js +319 -0
  17. package/src/client/components/model/Chat.js +119 -0
  18. package/src/client/components/model/Controls.js +120 -0
  19. package/src/client/components/model/Game.js +172 -0
  20. package/src/client/components/model/Lobby.js +226 -0
  21. package/src/client/components/model/LobbyAuth.js +161 -0
  22. package/src/client/components/model/Panel.js +70 -0
  23. package/src/client/components/model/Stat.js +77 -0
  24. package/src/client/components/model/Vote.js +216 -0
  25. package/src/client/components/view/Auth.js +167 -0
  26. package/src/client/components/view/CanvasManager.js +44 -0
  27. package/src/client/components/view/Chat.js +90 -0
  28. package/src/client/components/view/Controls.js +30 -0
  29. package/src/client/components/view/Game.js +48 -0
  30. package/src/client/components/view/Lobby.js +303 -0
  31. package/src/client/components/view/LobbyAuth.js +111 -0
  32. package/src/client/components/view/Panel.js +314 -0
  33. package/src/client/components/view/Stat.js +238 -0
  34. package/src/client/components/view/Vote.js +108 -0
  35. package/src/client/debug.js +158 -0
  36. package/src/client/lib/autostart.js +61 -0
  37. package/src/client/lib/contextTracker.js +46 -0
  38. package/src/client/lib/formBuilder.js +280 -0
  39. package/src/client/lib/hostGate.js +16 -0
  40. package/src/client/main.js +2016 -0
  41. package/src/client/network/HostConnectionManager.js +196 -0
  42. package/src/client/network/HostController.js +422 -0
  43. package/src/client/network/InlineHostBridge.js +152 -0
  44. package/src/client/network/LoopbackTransport.js +51 -0
  45. package/src/client/network/SignalingClient.js +146 -0
  46. package/src/client/network/WebRtcManager.js +147 -0
  47. package/src/client/network/WebSocketTransport.js +78 -0
  48. package/src/client/network/policyClose.js +54 -0
  49. package/src/client/network/workerSupport.js +34 -0
  50. package/src/client/providers/BakingProvider.js +88 -0
  51. package/src/client/providers/DependencyProvider.js +41 -0
  52. package/src/client/style.css +898 -0
  53. package/src/client/views/gameShell.js +190 -0
  54. package/src/client/views/includes/auth.pug +20 -0
  55. package/src/client/views/includes/chat.pug +5 -0
  56. package/src/client/views/includes/informer.pug +2 -0
  57. package/src/client/views/includes/lobby.pug +48 -0
  58. package/src/client/views/includes/lobbyAuth.pug +19 -0
  59. package/src/client/views/includes/panel.pug +3 -0
  60. package/src/client/views/includes/stat.pug +2 -0
  61. package/src/client/views/index.pug +8 -0
  62. package/src/config/closeCodes.js +18 -0
  63. package/src/config/env.js +61 -0
  64. package/src/devtools/ScenarioRunner.js +3 -8
  65. package/src/devtools/pluginLoader.js +8 -93
  66. package/src/host/HostGame.js +41 -3
  67. package/src/host/PortMachine.js +294 -0
  68. package/src/host/host.worker.js +29 -246
  69. package/src/host/identity.js +100 -0
  70. package/src/lib/clientIp.js +39 -0
  71. package/src/lib/loadGamePackage.js +122 -0
  72. package/src/lib/offlinePlayerData.js +18 -0
  73. package/src/standalone/index.js +125 -0
  74. 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
+ }
@@ -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 { validateAuth } from '../lib/validators.js';
14
- import { verifyIdentityToken } from '../lib/jwt.js';
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 socketManager = null;
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
- // порт-обработчики клиента (замыкание над gameId через state)
152
- function buildPortMethods(socketId, state) {
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 (!host || clients.has(socketId)) {
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
- onClientMessage(msg.socketId, msg.data);
176
+ portMachine?.message(msg.socketId, msg.data);
394
177
  break;
395
178
 
396
179
  case 'disconnect':
397
- onDisconnect(msg.socketId);
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(clients.keys()));
217
+ host?.completeHandoff(new Set(portMachine ? portMachine.socketIds : []));
435
218
  break;
436
219
  }
437
220
  };