vimp-engine 0.1.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 (45) hide show
  1. package/README.md +9 -0
  2. package/package.json +34 -0
  3. package/src/config/authClient.js +47 -0
  4. package/src/config/clientDefaults.js +113 -0
  5. package/src/config/hostDefaults.js +79 -0
  6. package/src/config/lobby.js +104 -0
  7. package/src/config/master.js +103 -0
  8. package/src/config/opcodes.js +29 -0
  9. package/src/config/wsports.js +35 -0
  10. package/src/host/GameCoreAdapter.js +206 -0
  11. package/src/host/HostGame.js +857 -0
  12. package/src/host/host.worker.js +438 -0
  13. package/src/host/meta/SocketManager.js +431 -0
  14. package/src/host/meta/core/CommandProcessor.js +99 -0
  15. package/src/host/meta/core/RoundManager.js +646 -0
  16. package/src/host/meta/core/VoteCoordinator.js +71 -0
  17. package/src/host/meta/modules/Panel.js +181 -0
  18. package/src/host/meta/modules/PlayerDataSync.js +176 -0
  19. package/src/host/meta/modules/RTTManager.js +168 -0
  20. package/src/host/meta/modules/Stat.js +294 -0
  21. package/src/host/meta/modules/TimerManager.js +277 -0
  22. package/src/host/meta/modules/Vote.js +179 -0
  23. package/src/host/meta/modules/chat/Chat.js +66 -0
  24. package/src/host/meta/modules/chat/index.js +1 -0
  25. package/src/host/meta/modules/chat/systemMessages.js +51 -0
  26. package/src/host/meta/player/HumanParticipant.js +36 -0
  27. package/src/host/meta/player/Participant.js +22 -0
  28. package/src/host/meta/player/ParticipantManager.js +247 -0
  29. package/src/host/meta/player/ScriptedParticipant.js +19 -0
  30. package/src/lib/AbstractTimer.js +84 -0
  31. package/src/lib/Publisher.js +43 -0
  32. package/src/lib/applyRoomOverrides.js +48 -0
  33. package/src/lib/buildClientConfig.js +48 -0
  34. package/src/lib/clientCoreConfig.js +59 -0
  35. package/src/lib/config.js +83 -0
  36. package/src/lib/coreConfig.js +60 -0
  37. package/src/lib/factory.js +21 -0
  38. package/src/lib/formatters.js +33 -0
  39. package/src/lib/gamePlugin.js +95 -0
  40. package/src/lib/jwt.js +103 -0
  41. package/src/lib/math.js +75 -0
  42. package/src/lib/rateLimiter.js +40 -0
  43. package/src/lib/sanitizers.js +17 -0
  44. package/src/lib/security.js +45 -0
  45. package/src/lib/validators.js +52 -0
@@ -0,0 +1,431 @@
1
+ const TECH_CODES = {
2
+ fullServer: [0],
3
+ anotherDevice: [1],
4
+ loading: [2],
5
+ kickIdle: [3],
6
+ kickForMaxLatency: [4],
7
+ kickForMissedPings: [5],
8
+ roomFull: [6], // P2P-комната заполнена (отказ без очереди ожидания)
9
+ };
10
+
11
+ const GAME_CODES = {
12
+ winnerTeam: [0],
13
+ roundStart: [1],
14
+ gameOver: [2],
15
+ };
16
+
17
+ export default class SocketManager {
18
+ /**
19
+ * @param {Object} ports - карта портов host→client (wsports.server).
20
+ * @param {Object} [gameOpts] - игровая параметризация (конфиг игры).
21
+ * @param {Object} [gameOpts.soundCues] - маппинг движковых событий на
22
+ * имена звуков игры: { roundStart, victory, defeat, frag, death }.
23
+ * @param {string} [gameOpts.initialVote] - имя голосования, отправляемого
24
+ * игроку после первого кадра (у танков — выбор команды).
25
+ */
26
+ constructor(ports, { soundCues = {}, initialVote = null } = {}) {
27
+ this._PORT_CONFIG_DATA = ports.CONFIG_DATA;
28
+ this._PORT_AUTH_DATA = ports.AUTH_DATA;
29
+ this._PORT_AUTH_RESULT = ports.AUTH_RESULT;
30
+ this._PORT_MAP_DATA = ports.MAP_DATA;
31
+ this._PORT_FIRST_SHOT_DATA = ports.FIRST_SHOT_DATA;
32
+ this._PORT_SHOT_DATA = ports.SHOT_DATA;
33
+ this._PORT_SOUND_DATA = ports.SOUND_DATA;
34
+ this._PORT_GAME_INFORM_DATA = ports.GAME_INFORM_DATA;
35
+ this._PORT_TECH_INFORM_DATA = ports.TECH_INFORM_DATA;
36
+ this._PORT_MISC = ports.MISC;
37
+ this._PORT_PING = ports.PING;
38
+ this._PORT_CLEAR = ports.CLEAR;
39
+ this._PORT_CONSOLE = ports.CONSOLE;
40
+ this._PORT_PANEL_DATA = ports.PANEL_DATA;
41
+ this._PORT_STAT_DATA = ports.STAT_DATA;
42
+ this._PORT_CHAT_DATA = ports.CHAT_DATA;
43
+ this._PORT_VOTE_DATA = ports.VOTE_DATA;
44
+ this._PORT_KEYSET_DATA = ports.KEYSET_DATA;
45
+
46
+ this._soundCues = soundCues;
47
+ this._initialVote = initialVote;
48
+
49
+ this._game = null;
50
+ this._panel = null;
51
+ this._stat = null;
52
+
53
+ this._senders = new Map();
54
+ this._binarySenders = new Map();
55
+ this._closers = new Map();
56
+ }
57
+
58
+ /**
59
+ * Инъекция игровых сервисов для формирования сложных сообщений.
60
+ * @param {Object} game - Экземпляр игрового менеджера.
61
+ * @param {Object} panel - Экземпляр менеджера панели.
62
+ * @param {Object} stat - Экземпляр менеджера статистики.
63
+ */
64
+ injectServices(game, panel, stat) {
65
+ this._game = game;
66
+ this._panel = panel;
67
+ this._stat = stat;
68
+ }
69
+
70
+ /**
71
+ * Регистрация пользователя в менеджере.
72
+ * @param {string} socketId - ID соединения.
73
+ * @param {Object} socket - Транспорт клиента с методами send и close.
74
+ */
75
+ addUser(socketId, socket) {
76
+ this._senders.set(socketId, socket.send.bind(socket));
77
+ this._binarySenders.set(socketId, socket.sendBinary.bind(socket));
78
+ this._closers.set(socketId, socket.close.bind(socket));
79
+ }
80
+
81
+ /**
82
+ * Удаление пользователя из менеджера.
83
+ * @param {string} socketId - ID соединения.
84
+ */
85
+ removeUser(socketId) {
86
+ this._senders.delete(socketId);
87
+ this._binarySenders.delete(socketId);
88
+ this._closers.delete(socketId);
89
+ }
90
+
91
+ /**
92
+ * Логирование ошибки отправки сообщения.
93
+ * @private
94
+ * @param {string} socketId
95
+ * @param {number} port
96
+ * @param {*} data
97
+ */
98
+ _logSendError(socketId, port, data) {
99
+ console.warn(`
100
+ [SocketManager Error]:
101
+ Attempted to send data to a non-existent or already closed socket.
102
+ - Socket ID: ${socketId}
103
+ - Port: ${port}
104
+ - Data (sample): ${JSON.stringify(data)?.substring(0, 300)}
105
+ `);
106
+ }
107
+
108
+ /**
109
+ * Логирование ошибки закрытия соединения.
110
+ * @private
111
+ * @param {string} socketId
112
+ * @param {number} code
113
+ * @param {*} data
114
+ */
115
+ _logCloseError(socketId, code, data) {
116
+ console.warn(`
117
+ [SocketManager Error]:
118
+ Attempted to close a non-existent or already closed socket.
119
+ - Socket ID: ${socketId}
120
+ - Close Code: ${code}
121
+ - Data (sample): ${JSON.stringify(data)?.substring(0, 300)}
122
+ `);
123
+ }
124
+
125
+ /**
126
+ * Отправка данных на клиент с проверкой наличия соединения.
127
+ * @private
128
+ * @param {string} socketId
129
+ * @param {number} port
130
+ * @param {*} data
131
+ * @param {boolean} [reliable] - надёжная ли доставка (WebRTC meta vs
132
+ * state).
133
+ */
134
+ _send(socketId, port, data, reliable = true) {
135
+ const sender = this._senders.get(socketId);
136
+
137
+ if (sender) {
138
+ sender(port, data, reliable);
139
+ } else {
140
+ this._logSendError(socketId, port, data);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Отправка бинарного кадра на клиент с проверкой наличия соединения.
146
+ * @private
147
+ * @param {string} socketId
148
+ * @param {ArrayBuffer} buffer - Кадр (порт — первый байт буфера).
149
+ * @param {boolean} [reliable] - надёжный ли кадр (WebRTC meta vs state).
150
+ */
151
+ _sendBinary(socketId, buffer, reliable) {
152
+ const sender = this._binarySenders.get(socketId);
153
+
154
+ if (sender) {
155
+ sender(buffer, reliable);
156
+ } else {
157
+ this._logSendError(socketId, 'binary', `<${buffer.byteLength} bytes>`);
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Закрытие соединения клиента с проверкой.
163
+ * @private
164
+ * @param {string} socketId
165
+ * @param {number} code
166
+ * @param {*} data
167
+ */
168
+ _close(socketId, code, data) {
169
+ const closer = this._closers.get(socketId);
170
+
171
+ if (closer) {
172
+ closer(code, data);
173
+ } else {
174
+ this._logCloseError(socketId, code, data);
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Закрытие соединения с отправкой технического сообщения.
180
+ * @param {string} socketId
181
+ * @param {number} code - Код закрытия.
182
+ * @param {string} [key] - Ключ технического события (TECH_CODES).
183
+ * @param {Array|undefined} [arr] - Дополнительные параметры.
184
+ */
185
+ close(socketId, code, key, arr) {
186
+ if (key) {
187
+ const data = Array.isArray(arr)
188
+ ? [...TECH_CODES[key], arr]
189
+ : TECH_CODES[key];
190
+
191
+ this._close(socketId, code, data);
192
+ } else {
193
+ this._close(socketId, code);
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Отправка конфигурационных данных.
199
+ * @param {string} socketId
200
+ * @param {*} config
201
+ */
202
+ sendConfig(socketId, config) {
203
+ this._send(socketId, this._PORT_CONFIG_DATA, config);
204
+ }
205
+
206
+ /**
207
+ * Отправка данных для авторизации.
208
+ * @param {string} socketId
209
+ * @param {*} authData
210
+ */
211
+ sendAuthData(socketId, authData) {
212
+ this._send(socketId, this._PORT_AUTH_DATA, authData);
213
+ }
214
+
215
+ /**
216
+ * Отправка данных о результате авторизации.
217
+ * @param {string} socketId
218
+ * @param {*} data
219
+ */
220
+ sendAuthResult(socketId, data) {
221
+ this._send(socketId, this._PORT_AUTH_RESULT, data);
222
+ }
223
+
224
+ /**
225
+ * Отправка ping для измерения RTT. Идёт ненадёжным каналом (WebRTC state):
226
+ * замер отражает реальный сетевой путь, а не reliable-поток meta с его
227
+ * ретрансмиссиями; потерянный ping покрывается допуском maxMissedPings.
228
+ * @param {string} socketId
229
+ * @param {number} pingIdCounter
230
+ */
231
+ sendPing(socketId, pingIdCounter) {
232
+ this._send(socketId, this._PORT_PING, pingIdCounter, false);
233
+ }
234
+
235
+ /**
236
+ * Отправка команды очистки данных.
237
+ * @param {string} socketId
238
+ * @param {Array|string} [setIdList]
239
+ */
240
+ sendClear(socketId, setIdList) {
241
+ if (setIdList) {
242
+ this._send(socketId, this._PORT_CLEAR, setIdList);
243
+ } else {
244
+ this._send(socketId, this._PORT_CLEAR);
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Отправка технического сообщения.
250
+ * @param {string} socketId
251
+ * @param {string} [key] - Ключ технического события (TECH_CODES).
252
+ * @param {Array|undefined} [arr] - Дополнительные параметры.
253
+ */
254
+ sendTechInform(socketId, key, arr) {
255
+ if (key) {
256
+ const data = Array.isArray(arr)
257
+ ? [...TECH_CODES[key], arr]
258
+ : TECH_CODES[key];
259
+
260
+ this._send(socketId, this._PORT_TECH_INFORM_DATA, data);
261
+ } else {
262
+ this._send(socketId, this._PORT_TECH_INFORM_DATA);
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Отправка данных карты.
268
+ * @param {string} socketId
269
+ * @param {*} mapData
270
+ */
271
+ sendMap(socketId, mapData) {
272
+ this._send(socketId, this._PORT_MAP_DATA, mapData);
273
+ }
274
+
275
+ /**
276
+ * Отправка первого кадра игры. Snapshot идёт на FIRST_SHOT_DATA,
277
+ * полная статистика, панель и набор клавиш — своими каналами.
278
+ * @param {string} socketId
279
+ */
280
+ sendFirstShot(socketId) {
281
+ // camera = 0 (координат на первом кадре нет); seq = 0 (одноразовый кадр)
282
+ this._send(socketId, this._PORT_FIRST_SHOT_DATA, [
283
+ this._game.getPlayersData(), // gameSnapshot
284
+ 0, // camera
285
+ Date.now(), // serverTime
286
+ 0, // seq
287
+ ]);
288
+ this.sendStat(socketId, this._stat.getFull());
289
+ this.sendPanel(socketId, this._panel.getEmptyPanel());
290
+ this.sendKeySet(socketId, 0); // наблюдатель
291
+ }
292
+
293
+ /**
294
+ * Отправка первого голосования (initialVote из конфига игры).
295
+ * @param {string} socketId
296
+ */
297
+ sendFirstVote(socketId) {
298
+ if (this._initialVote) {
299
+ this.sendVote(socketId, { name: this._initialVote });
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Отправка игровых данных (бинарный snapshot-кадр).
305
+ * @param {string} socketId
306
+ * @param {ArrayBuffer} frameBuffer - Кадр из pack_frame ядра.
307
+ * @param {boolean} [reliable] - событийный ли кадр (WebRTC meta) или
308
+ * позиционный (state).
309
+ */
310
+ sendShot(socketId, frameBuffer, reliable) {
311
+ this._sendBinary(socketId, frameBuffer, reliable);
312
+ }
313
+
314
+ /**
315
+ * Отправка данных панели.
316
+ * @param {string} socketId
317
+ * @param {*} data
318
+ */
319
+ sendPanel(socketId, data) {
320
+ this._send(socketId, this._PORT_PANEL_DATA, data);
321
+ }
322
+
323
+ /**
324
+ * Отправка данных статистики.
325
+ * @param {string} socketId
326
+ * @param {*} data
327
+ */
328
+ sendStat(socketId, data) {
329
+ this._send(socketId, this._PORT_STAT_DATA, data);
330
+ }
331
+
332
+ /**
333
+ * Отправка сообщения чата.
334
+ * @param {string} socketId
335
+ * @param {*} data
336
+ */
337
+ sendChat(socketId, data) {
338
+ this._send(socketId, this._PORT_CHAT_DATA, data);
339
+ }
340
+
341
+ /**
342
+ * Отправка данных голосования.
343
+ * @param {string} socketId
344
+ * @param {*} data
345
+ */
346
+ sendVote(socketId, data) {
347
+ this._send(socketId, this._PORT_VOTE_DATA, data);
348
+ }
349
+
350
+ /**
351
+ * Отправка набора клавиш (смена режима спектатор/игрок).
352
+ * @param {string} socketId
353
+ * @param {number} keySet - 0 (наблюдатель) | 1 (игрок)
354
+ */
355
+ sendKeySet(socketId, keySet) {
356
+ this._send(socketId, this._PORT_KEYSET_DATA, keySet);
357
+ }
358
+
359
+ /**
360
+ * Отправка базовых данных игрока (полная панель + набор клавиш игрока).
361
+ * @param {string} socketId
362
+ * @param {string} gameId - ID игрока в игре.
363
+ */
364
+ sendPlayerDefaultShot(socketId, gameId) {
365
+ this.sendPanel(socketId, this._panel.getFullPanel(gameId));
366
+ this.sendKeySet(socketId, 1);
367
+ }
368
+
369
+ /**
370
+ * Отправка базовых данных наблюдателя (пустая панель + набор клавиш наблюдателя).
371
+ * @param {string} socketId
372
+ */
373
+ sendSpectatorDefaultShot(socketId) {
374
+ this.sendPanel(socketId, this._panel.getEmptyPanel());
375
+ this.sendKeySet(socketId, 0);
376
+ }
377
+
378
+ /**
379
+ * Отправка звукового сигнала движкового события. Маппинг события на имя
380
+ * звука задаёт игра (soundCues); незамапленный сигнал не отправляется.
381
+ * @param {string} socketId
382
+ * @param {string} cue - движковое событие (roundStart, victory, defeat,
383
+ * frag, death).
384
+ */
385
+ sendSoundCue(socketId, cue) {
386
+ const sound = this._soundCues[cue];
387
+
388
+ if (sound) {
389
+ this._send(socketId, this._PORT_SOUND_DATA, sound);
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Отправка игрового информера.
395
+ * @param {string} socketId
396
+ * @param {string} key - Ключ игрового события (GAME_CODES).
397
+ * @param {Array|undefined} [arr] - Дополнительные параметры.
398
+ */
399
+ sendGameInform(socketId, key, arr) {
400
+ const data = Array.isArray(arr)
401
+ ? [...GAME_CODES[key], arr]
402
+ : GAME_CODES[key];
403
+
404
+ this._send(socketId, this._PORT_GAME_INFORM_DATA, data);
405
+ }
406
+
407
+ /**
408
+ * Отправка информации об окончании раунда.
409
+ * @param {string} socketId
410
+ * @param {string|number} [winnerTeam] - Победившая команда.
411
+ */
412
+ sendRoundEnd(socketId, winnerTeam) {
413
+ if (winnerTeam) {
414
+ this.sendGameInform(socketId, 'winnerTeam', [winnerTeam]);
415
+ } else {
416
+ this.sendGameInform(socketId, 'gameOver');
417
+ }
418
+ }
419
+
420
+ /**
421
+ * Отправка команды смены имени.
422
+ * @param {string} socketId
423
+ * @param {string} name
424
+ */
425
+ sendName(socketId, name) {
426
+ this._send(socketId, this._PORT_MISC, {
427
+ key: 'localstorageNameReplace',
428
+ value: name,
429
+ });
430
+ }
431
+ }
@@ -0,0 +1,99 @@
1
+ // Обработчик чат-команд. Движковое ядро: /name, /nr, /timeleft, /mapname,
2
+ // /rank; игровые команды (HostPlugin.chatCommands) регистрируются через
3
+ // registerCommand и получают контекст меты: handler(ctx, gameId, args).
4
+ class CommandProcessor {
5
+ constructor(deps) {
6
+ this._chat = deps.chat;
7
+ this._roundManager = deps.roundManager;
8
+ this._timerManager = deps.timerManager;
9
+ this._playerDataSync = deps.playerDataSync;
10
+ this._isDevMode = deps.isDevMode;
11
+
12
+ // контекст игровых команд (participants, chat, scripted, roundManager,
13
+ // voteCoordinator, teams, spectatorTeam, spectatorId, …)
14
+ this._ctx = deps;
15
+
16
+ this._commands = new Map();
17
+ }
18
+
19
+ /**
20
+ * Регистрирует игровую команду.
21
+ * @param {string} name - имя команды (с ведущим '/').
22
+ * @param {Function} handler - обработчик (ctx, gameId, args) => void.
23
+ */
24
+ registerCommand(name, handler) {
25
+ this._commands.set(name, handler);
26
+ }
27
+
28
+ // обрабатывает команду от пользователя
29
+ parseCommand(gameId, message) {
30
+ message = message.replace(/\s\s+/g, ' ');
31
+
32
+ const arr = message.split(' ');
33
+ const cmd = arr.shift();
34
+
35
+ switch (cmd) {
36
+ // смена ника
37
+ case '/name':
38
+ this._roundManager.changeName(gameId, arr.join(' '));
39
+ break;
40
+
41
+ // новый раунд
42
+ case '/nr':
43
+ if (this._isDevMode) {
44
+ this._roundManager.initiateNewRound();
45
+ } else {
46
+ this._chat.pushSystemByUser(gameId, 'COMMANDS_NOT_FOUND');
47
+ }
48
+ break;
49
+
50
+ // время карты
51
+ case '/timeleft': {
52
+ function getTime(ms) {
53
+ const totalSeconds = Math.floor(ms / 1000);
54
+ let minutes = Math.floor(totalSeconds / 60);
55
+ let seconds = totalSeconds % 60;
56
+
57
+ if (minutes < 10) {
58
+ minutes = '0' + minutes;
59
+ }
60
+
61
+ if (seconds < 10) {
62
+ seconds = '0' + seconds;
63
+ }
64
+
65
+ return `${minutes}:${seconds}`;
66
+ }
67
+
68
+ this._chat.pushSystemByUser(gameId, [
69
+ getTime(this._timerManager.getMapTimeLeft()),
70
+ ]);
71
+ break;
72
+ }
73
+
74
+ // название текущей карты
75
+ case '/mapname':
76
+ this._chat.pushSystemByUser(gameId, [this._roundManager.currentMap]);
77
+ break;
78
+
79
+ // ранг игрока (Этап B4/B5: PlayerDataSync, подгружен с auth-сервиса)
80
+ case '/rank':
81
+ this._chat.pushSystemByUser(gameId, 'RANK', [
82
+ this._playerDataSync.getRank(gameId),
83
+ ]);
84
+ break;
85
+
86
+ default: {
87
+ const handler = this._commands.get(cmd);
88
+
89
+ if (handler) {
90
+ handler(this._ctx, gameId, arr);
91
+ } else {
92
+ this._chat.pushSystemByUser(gameId, 'COMMANDS_NOT_FOUND');
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ export default CommandProcessor;