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.
- package/README.md +9 -0
- package/package.json +34 -0
- package/src/config/authClient.js +47 -0
- package/src/config/clientDefaults.js +113 -0
- package/src/config/hostDefaults.js +79 -0
- package/src/config/lobby.js +104 -0
- package/src/config/master.js +103 -0
- package/src/config/opcodes.js +29 -0
- package/src/config/wsports.js +35 -0
- package/src/host/GameCoreAdapter.js +206 -0
- package/src/host/HostGame.js +857 -0
- package/src/host/host.worker.js +438 -0
- package/src/host/meta/SocketManager.js +431 -0
- package/src/host/meta/core/CommandProcessor.js +99 -0
- package/src/host/meta/core/RoundManager.js +646 -0
- package/src/host/meta/core/VoteCoordinator.js +71 -0
- package/src/host/meta/modules/Panel.js +181 -0
- package/src/host/meta/modules/PlayerDataSync.js +176 -0
- package/src/host/meta/modules/RTTManager.js +168 -0
- package/src/host/meta/modules/Stat.js +294 -0
- package/src/host/meta/modules/TimerManager.js +277 -0
- package/src/host/meta/modules/Vote.js +179 -0
- package/src/host/meta/modules/chat/Chat.js +66 -0
- package/src/host/meta/modules/chat/index.js +1 -0
- package/src/host/meta/modules/chat/systemMessages.js +51 -0
- package/src/host/meta/player/HumanParticipant.js +36 -0
- package/src/host/meta/player/Participant.js +22 -0
- package/src/host/meta/player/ParticipantManager.js +247 -0
- package/src/host/meta/player/ScriptedParticipant.js +19 -0
- package/src/lib/AbstractTimer.js +84 -0
- package/src/lib/Publisher.js +43 -0
- package/src/lib/applyRoomOverrides.js +48 -0
- package/src/lib/buildClientConfig.js +48 -0
- package/src/lib/clientCoreConfig.js +59 -0
- package/src/lib/config.js +83 -0
- package/src/lib/coreConfig.js +60 -0
- package/src/lib/factory.js +21 -0
- package/src/lib/formatters.js +33 -0
- package/src/lib/gamePlugin.js +95 -0
- package/src/lib/jwt.js +103 -0
- package/src/lib/math.js +75 -0
- package/src/lib/rateLimiter.js +40 -0
- package/src/lib/sanitizers.js +17 -0
- package/src/lib/security.js +45 -0
- 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;
|