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,71 @@
1
+ // Координатор голосований: создание, кулдаун и сброс опросов.
2
+ // Инфраструктура поверх Vote + TimerManager; не знает о round/map-логике
3
+ // (вызывающие передают готовые payload и resultFunc).
4
+ class VoteCoordinator {
5
+ constructor({ vote, chat, timerManager }) {
6
+ this._vote = vote;
7
+ this._chat = chat;
8
+ this._timerManager = timerManager;
9
+ }
10
+
11
+ // проверяет возможность создать голосование в данной категории
12
+ canCreateVote(voteCategory, gameId) {
13
+ if (
14
+ this._timerManager.isVoteBlocked(voteCategory) ||
15
+ this._vote.hasVoteCategory(voteCategory)
16
+ ) {
17
+ if (gameId) {
18
+ this._chat.pushSystemByUser(gameId, 'VOTE_UNAVAILABLE');
19
+ }
20
+
21
+ return false;
22
+ }
23
+
24
+ return true;
25
+ }
26
+
27
+ // создаёт опрос
28
+ createVote(data) {
29
+ const { voteName, voteCategory, payload, resultFunc, userList, gameId } =
30
+ data;
31
+
32
+ // если голосование инициировано пользователем
33
+ if (gameId) {
34
+ this._chat.pushSystemByUser(gameId, 'VOTE_CREATED');
35
+ }
36
+
37
+ const onStart = () => {
38
+ if (gameId) {
39
+ this._chat.pushSystemByUser(gameId, 'VOTE_STARTED');
40
+ this._vote.addInVote(voteName, 'Yes');
41
+ }
42
+
43
+ // таймер на сбор результатов
44
+ this._timerManager.startVoteTimer(voteName, () => {
45
+ // таймер временной блокировки повторного голосования
46
+ this._timerManager.startVoteBlockTimer(voteCategory, () => {});
47
+
48
+ const result = this._vote.getResult(voteName);
49
+
50
+ resultFunc(result);
51
+ });
52
+ };
53
+
54
+ this._vote.createVote({
55
+ name: voteName,
56
+ category: voteCategory,
57
+ payload,
58
+ userList,
59
+ onStartCallback: onStart,
60
+ });
61
+ }
62
+
63
+ // сбрасывает активные и запланированные опросы и таймеры по ним
64
+ reset() {
65
+ this._timerManager.stopAllVoteTimers();
66
+ this._timerManager.stopAllBlockedVoteTimers();
67
+ this._vote.reset();
68
+ }
69
+ }
70
+
71
+ export default VoteCoordinator;
@@ -0,0 +1,181 @@
1
+ // Singleton Panel
2
+
3
+ let panel;
4
+
5
+ class Panel {
6
+ // config — схема панели игры: { fields: { имя: { key, value } }, activeKey }
7
+ constructor(config) {
8
+ if (panel) {
9
+ return panel;
10
+ }
11
+
12
+ panel = this;
13
+
14
+ this._fields = config.fields;
15
+ this._activeKey = config.activeKey;
16
+
17
+ this._data = {};
18
+ this._timerManager = null;
19
+
20
+ this._emptyPanel = Object.values(this._fields).map(item => item.key);
21
+ this._defaultPanel = {};
22
+
23
+ this._lastSentRoundTime = -1; // кеширование последнего значения времени
24
+
25
+ for (const key of Object.keys(this._fields)) {
26
+ this._defaultPanel[key] = this._fields[key].value;
27
+ }
28
+ }
29
+
30
+ // внедряет зависимость TimerManager
31
+ injectTimerManager(timerManager) {
32
+ this._timerManager = timerManager;
33
+ this._lastSentRoundTime = this._timerManager.getRoundTimeLeft();
34
+ }
35
+
36
+ // сбрасывает внутреннее состояние пользователей к дефолтному
37
+ reset() {
38
+ for (const gameId in this._data) {
39
+ if (Object.hasOwn(this._data, gameId)) {
40
+ const user = this._data[gameId];
41
+
42
+ user.values = { ...this._defaultPanel };
43
+ user.pendingChanges = {}; // очистка, без добавления новых данных
44
+ }
45
+ }
46
+ }
47
+
48
+ // добавляет пользователя
49
+ addUser(gameId) {
50
+ this._data[gameId] = {
51
+ values: { ...this._defaultPanel },
52
+ pendingChanges: {}, // объект для хранения только измененных данных
53
+ };
54
+ }
55
+
56
+ // удаляет пользователя
57
+ removeUser(gameId) {
58
+ delete this._data[gameId];
59
+ }
60
+
61
+ // очищает ожидающие изменения для пользователя
62
+ // требуется, когда состояние пользователя резко меняется
63
+ // (например при уничтожении)
64
+ invalidate(gameId) {
65
+ const user = this._data[gameId];
66
+
67
+ if (user) {
68
+ user.pendingChanges = {};
69
+ }
70
+ }
71
+
72
+ // обновляет данные пользователя
73
+ // param: имя поля из схемы (например, 'health', 'w1')
74
+ // value: значение
75
+ // operation: 'set', 'decrement', 'increment'
76
+ updateUser(gameId, param, value, operation = 'decrement') {
77
+ const user = this._data[gameId];
78
+ const values = user.values;
79
+ const currentValue = values[param];
80
+ let newValue;
81
+
82
+ if (operation === 'set') {
83
+ newValue = value;
84
+ } else if (operation === 'decrement') {
85
+ newValue = currentValue - value;
86
+ } else if (operation === 'increment') {
87
+ newValue = currentValue + value;
88
+ }
89
+
90
+ if (newValue < 0) {
91
+ newValue = 0;
92
+ }
93
+
94
+ values[param] = newValue;
95
+ user.pendingChanges[this._fields[param].key] = newValue;
96
+ }
97
+
98
+ // устанавливает активное оружие (ключ кадра — activeKey схемы)
99
+ setActiveWeapon(gameId, weaponKey) {
100
+ const user = this._data[gameId];
101
+ user.pendingChanges[this._activeKey] = weaponKey;
102
+ }
103
+
104
+ // проверяет, достаточно ли у пользователя ресурсов для действия
105
+ hasResources(gameId, param, value) {
106
+ const user = this._data[gameId];
107
+ const currentValue = user.values[param];
108
+
109
+ return currentValue >= value;
110
+ }
111
+
112
+ // возвращает текущее значение параметра для пользователя
113
+ getCurrentValue(gameId, param) {
114
+ const user = this._data[gameId];
115
+
116
+ return user.values[param];
117
+ }
118
+
119
+ // вычисляет все обновления панели за один тик
120
+ processUpdates() {
121
+ const updates = {};
122
+ const currentTime = this._timerManager.getRoundTimeLeft();
123
+ const timeChanged = currentTime !== this._lastSentRoundTime;
124
+
125
+ if (timeChanged) {
126
+ this._lastSentRoundTime = currentTime;
127
+ }
128
+
129
+ for (const gameId in this._data) {
130
+ if (Object.hasOwn(this._data, gameId)) {
131
+ const user = this._data[gameId];
132
+ const userChanges = user.pendingChanges;
133
+ const userData = [];
134
+
135
+ if (timeChanged) {
136
+ userData.push(`t:${currentTime}`);
137
+ }
138
+
139
+ for (const key in userChanges) {
140
+ if (Object.hasOwn(userChanges, key)) {
141
+ userData.push(`${key}:${userChanges[key]}`);
142
+ }
143
+ }
144
+
145
+ if (userData.length) {
146
+ updates[gameId] = userData;
147
+ }
148
+
149
+ user.pendingChanges = {};
150
+ }
151
+ }
152
+
153
+ return updates;
154
+ }
155
+
156
+ // возвращает полный набор данных для инициализации панели игрока
157
+ getFullPanel(gameId) {
158
+ const user = this._data[gameId];
159
+ user.pendingChanges = {}; // очистка старых изменений
160
+
161
+ const panelData = [`t:${this._lastSentRoundTime}`];
162
+ const values = user.values;
163
+
164
+ for (const param in values) {
165
+ if (Object.hasOwn(values, param)) {
166
+ const key = this._fields[param].key;
167
+ const value = values[param];
168
+
169
+ panelData.push(`${key}:${value}`);
170
+ }
171
+ }
172
+ return panelData;
173
+ }
174
+
175
+ // возвращает пустые данные (ключи без значений)
176
+ getEmptyPanel() {
177
+ return [`t:${this._lastSentRoundTime}`].concat(this._emptyPanel);
178
+ }
179
+ }
180
+
181
+ export default Panel;
@@ -0,0 +1,176 @@
1
+ import lobbyConfig from '../../../config/lobby.js';
2
+
3
+ // Rank/state участника (Этап B4). Игрок приходит в игру с ником из JWT —
4
+ // rank (числовой рейтинг) и state (непрозрачный для движка JSON, "скиллы")
5
+ // подгружаются с мастера (прокси central auth-сервиса) при входе и
6
+ // синхронизируются обратно по естественным границам жизненного цикла
7
+ // (RoundManager: смена карты, конец раунда). Схему/дефолты state объявляет
8
+ // игра — здесь это чёрный ящик.
9
+ export default class PlayerDataSync {
10
+ constructor(gameId, { fetchImpl = fetch, defaultState = {} } = {}) {
11
+ this._gameId = gameId;
12
+ this._fetch = fetchImpl;
13
+ this._defaultState = defaultState;
14
+ this._entries = new Map(); // participantId -> { token, rank, state }
15
+ // hostId + per-room секрет комнаты (кодревью №1, plan/server-rating/
16
+ // review.md): неизвестны при создании (назначаются мастером на
17
+ // register_host, после запуска Worker'а) — проставляются позже через
18
+ // setHostId(). Едут в теле PUT rank/state; мастер сверяет секрет с
19
+ // реестром и по нему подставляет проверенную атрибуцию, не доверяя
20
+ // hostId из тела напрямую (иначе можно было бы подставить чужую комнату)
21
+ this._hostId = null;
22
+ this._hostSecret = null;
23
+ }
24
+
25
+ // вызывается HostGame, когда мастер подтвердил регистрацию комнаты
26
+ // (host_registered) — до этого flush уходит без атрибуции
27
+ setHostId(hostId, hostSecret = null) {
28
+ this._hostId = hostId;
29
+ this._hostSecret = hostSecret;
30
+ }
31
+
32
+ _authedFetch(url, token, { method = 'GET', body } = {}) {
33
+ return this._fetch(`${url}?game=${this._gameId}`, {
34
+ method,
35
+ headers: {
36
+ authorization: `Bearer ${token}`,
37
+ ...(body ? { 'content-type': 'application/json' } : {}),
38
+ },
39
+ body: body ? JSON.stringify(body) : undefined,
40
+ });
41
+ }
42
+
43
+ // подгружает rank+state с мастера при входе игрока. Сбой auth-сервиса не
44
+ // должен блокировать вход — участник стартует с дефолтами (rank 0, пустой
45
+ // state) и попробует синхронизироваться на следующем flush.
46
+ // rankLoaded/stateLoaded (F4 кодревью) отражают, был ли реально получен
47
+ // серверный rank/state — flush не должен PUT'ить дефолт поверх настоящих
48
+ // сохранённых значений, если загрузка не удалась (auth недоступен на join)
49
+ async load(participantId, token) {
50
+ const entry = this._entries.get(participantId) ?? {
51
+ token,
52
+ rank: 0,
53
+ // server-rating этап 1: PUT /auth/rank шлёт дельту матча, не абсолют
54
+ // (auth ведёт леджер) — pendingRankDelta копит то, что ещё не flush'нуто
55
+ pendingRankDelta: 0,
56
+ state: structuredClone(this._defaultState),
57
+ rankLoaded: false,
58
+ stateLoaded: false,
59
+ };
60
+
61
+ entry.token = token;
62
+ this._entries.set(participantId, entry);
63
+
64
+ try {
65
+ const [rankRes, stateRes] = await Promise.all([
66
+ this._authedFetch(lobbyConfig.auth.rankUrl, token),
67
+ this._authedFetch(lobbyConfig.auth.stateUrl, token),
68
+ ]);
69
+
70
+ if (rankRes.ok && !entry.rankLoaded) {
71
+ // F9: во время await мог накопиться addRank-дельта поверх
72
+ // стартового 0 — прибавляем, а не перетираем серверным значением
73
+ entry.rank += (await rankRes.json()).rank ?? 0;
74
+ entry.rankLoaded = true;
75
+ }
76
+
77
+ if (stateRes.ok && !entry.stateLoaded) {
78
+ const { state } = await stateRes.json();
79
+
80
+ entry.state =
81
+ state && Object.keys(state).length ? state : structuredClone(this._defaultState);
82
+ entry.stateLoaded = true;
83
+ }
84
+ } catch {
85
+ // недоступность auth-сервиса — остаёмся на дефолтах, следующий
86
+ // flush повторит load() перед синхронизацией (см. flush)
87
+ }
88
+
89
+ return entry;
90
+ }
91
+
92
+ getRank(participantId) {
93
+ return this._entries.get(participantId)?.rank ?? 0;
94
+ }
95
+
96
+ getState(participantId) {
97
+ return this._entries.get(participantId)?.state ?? this._defaultState;
98
+ }
99
+
100
+ // прибавляет к рангу игрока (вызывается из RoundManager.reportKill —
101
+ // тот же чокпоинт, что и Stat score)
102
+ addRank(participantId, delta) {
103
+ const entry = this._entries.get(participantId);
104
+
105
+ if (entry) {
106
+ entry.rank += delta;
107
+ entry.pendingRankDelta += delta;
108
+ }
109
+ }
110
+
111
+ setState(participantId, state) {
112
+ const entry = this._entries.get(participantId);
113
+
114
+ if (entry) {
115
+ entry.state = state;
116
+ }
117
+ }
118
+
119
+ removeUser(participantId) {
120
+ this._entries.delete(participantId);
121
+ }
122
+
123
+ // синхронизирует накопленные rank+state участника на мастер. Сбой не
124
+ // бросается дальше — следующий flush попробует снова с уже накопленными
125
+ // (не потерянными) данными. F4: если исходная load() не удалась, PUT
126
+ // дефолтом затёр бы реальный сохранённый rank/state — вместо этого
127
+ // повторяем load() и шлём PUT только для того, что реально загрузилось
128
+ async flush(participantId) {
129
+ let entry = this._entries.get(participantId);
130
+
131
+ if (!entry) {
132
+ return;
133
+ }
134
+
135
+ if (!entry.rankLoaded || !entry.stateLoaded) {
136
+ entry = await this.load(participantId, entry.token);
137
+ }
138
+
139
+ const { token, state, rankLoaded, stateLoaded } = entry;
140
+ const requests = [];
141
+
142
+ if (rankLoaded) {
143
+ // отправляем именно накопленную на этот момент дельту и вычитаем её
144
+ // же после успеха — addRank во время await не теряется (тот же
145
+ // паттерн, что F9 в load())
146
+ const delta = entry.pendingRankDelta;
147
+
148
+ requests.push(
149
+ this._authedFetch(lobbyConfig.auth.rankUrl, token, {
150
+ method: 'PUT',
151
+ body: { delta, hostId: this._hostId, hostSecret: this._hostSecret },
152
+ }).then(res => {
153
+ if (res.ok) {
154
+ entry.pendingRankDelta -= delta;
155
+ }
156
+ }),
157
+ );
158
+ }
159
+
160
+ if (stateLoaded) {
161
+ requests.push(this._authedFetch(lobbyConfig.auth.stateUrl, token, {
162
+ method: 'PUT',
163
+ body: { state, hostId: this._hostId, hostSecret: this._hostSecret },
164
+ }));
165
+ }
166
+
167
+ await Promise.allSettled(requests);
168
+ }
169
+
170
+ // синхронизирует всех текущих участников (границы раунда/карты)
171
+ flushAll() {
172
+ return Promise.allSettled(
173
+ [...this._entries.keys()].map(id => this.flush(id)),
174
+ );
175
+ }
176
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * @class RTTManager
3
+ * @description Управляет расчетом Round Trip Time (RTT) для пользователя,
4
+ * отслеживает пропущенные пинги и инициирует кик игроков за высокую задержку
5
+ * или отсутствие ответов.
6
+ */
7
+ export default class RTTManager {
8
+ /**
9
+ * @param {object} params - Параметры конфигурации RTT.
10
+ * @param {number} params.maxMissedPings - Максимальное количество
11
+ * пропущенных пингов перед киком.
12
+ * @param {number} params.maxLatency - Максимально допустимая
13
+ * задержка RTT (в мс) перед киком.
14
+ * @param {object} callbacks - Колбэки для взаимодействия с внешним модулем.
15
+ * @param {function(string): void} callbacks.onKickForMissedPings -
16
+ * Колбэк при кике за пропущенные пинги.
17
+ * @param {function(string): void} callbacks.onKickForMaxLatency -
18
+ * Колбэк при кике за высокую задержку.
19
+ */
20
+ constructor(params, callbacks) {
21
+ this._maxMissedPings = params.maxMissedPings;
22
+ this._maxLatency = params.maxLatency;
23
+ this._callbacks = callbacks;
24
+
25
+ /**
26
+ * @private
27
+ * @description Коэффициент сглаживания для
28
+ * экспоненциального скользящего среднего (EMA).
29
+ * Определяет, насколько сильно новый замер RTT влияет на текущее значение.
30
+ */
31
+ this._alpha = 0.1;
32
+
33
+ /**
34
+ * @private
35
+ * @description Коллекция Map для хранения RTT-данных
36
+ * по каждому пользователю.
37
+ * @type {Map<string, {
38
+ * rtt: number,
39
+ * missedPings: number,
40
+ * outstandingPings: Map<number, number>,
41
+ * pingIdCounter: number
42
+ * }>
43
+ * }
44
+ * - rtt: сглаженное значение RTT
45
+ * - missedPings: счетчик пропущенных пингов подряд
46
+ * - outstandingPings: Map для отслеживания отправленных,
47
+ * но еще не отвеченных пингов { pingID: time }
48
+ * - pingIdCounter: счетчик ID для следующего пинга
49
+ */
50
+ this._users = new Map();
51
+ }
52
+
53
+ /**
54
+ * @description Добавляет нового пользователя в систему отслеживания RTT.
55
+ * @param {string} gameId - Уникальный идентификатор пользователя.
56
+ */
57
+ addUser(gameId) {
58
+ this._users.set(gameId, {
59
+ rtt: 100, // начальное значение RTT (предположение)
60
+ missedPings: 0,
61
+ outstandingPings: new Map(),
62
+ pingIdCounter: 0,
63
+ });
64
+ }
65
+
66
+ /**
67
+ * @description Удаляет пользователя из системы отслеживания RTT.
68
+ * @param {string} gameId - Уникальный идентификатор пользователя.
69
+ */
70
+ removeUser(gameId) {
71
+ this._users.delete(gameId);
72
+ }
73
+
74
+ /**
75
+ * @description Планирует следующий пинг для всех пользователей.
76
+ * Обновляет счетчики пропущенных пингов и готовит новые ID для отправки.
77
+ * Инициирует кик для игроков, превысивших лимит пропущенных пингов.
78
+ * @returns {Iterator<[string, object]>} -
79
+ * Итератор пар [gameId, userData] для отправки пингов.
80
+ */
81
+ scheduleNextPing() {
82
+ const toKick = [];
83
+
84
+ this._users.forEach((user, gameId) => {
85
+ // если с прошлого раза остался неотвеченный пинг,
86
+ // он считается пропущенным
87
+ if (user.outstandingPings.size > 0) {
88
+ user.missedPings += 1;
89
+
90
+ // если пользователь превысил лимит пропущенных пингов
91
+ if (this._maxMissedPings && user.missedPings >= this._maxMissedPings) {
92
+ toKick.push(gameId);
93
+ return; // пропускается отправка нового пинга
94
+ }
95
+ }
96
+
97
+ // новый пинг
98
+ user.pingIdCounter += 1;
99
+ const time = Date.now();
100
+
101
+ // удаление старых пингов и запись нового
102
+ // `outstandingPings` содержит только один, последний отправленный пинг
103
+ user.outstandingPings.clear();
104
+ user.outstandingPings.set(user.pingIdCounter, time);
105
+ });
106
+
107
+ // кики после завершения итерации по коллекции
108
+ toKick.forEach(gameId => {
109
+ this._callbacks.onKickForMissedPings(gameId);
110
+ });
111
+
112
+ // итератор для внешнего модуля, для отправки пингов
113
+ return this._users.entries();
114
+ }
115
+
116
+ /**
117
+ * @description Обрабатывает ответное сообщение 'pong' от клиента.
118
+ * Вычисляет новый RTT, обновляет статистику и проверяет
119
+ * на превышение максимальной задержки.
120
+ * @param {string} gameId - Идентификатор пользователя,
121
+ * от которого пришел 'pong'.
122
+ * @param {number} pingId - ID пинга, на который пришел ответ.
123
+ * @returns {number|null} - Рассчитанное значение RTT (latency) или null,
124
+ * если 'pong' некорректен.
125
+ */
126
+ handlePong(gameId, pingId) {
127
+ const user = this._users.get(gameId);
128
+
129
+ // если пользователь был удален, пока 'pong' был в пути
130
+ if (!user) {
131
+ return null;
132
+ }
133
+
134
+ // игнорование ответов на устаревшие пинги,
135
+ // чтобы избежать ложных срабатываний
136
+ // обработка ответа на самый последний отправленный пинг
137
+ if (pingId !== user.pingIdCounter) {
138
+ return null;
139
+ }
140
+
141
+ const time = user.outstandingPings.get(pingId);
142
+
143
+ if (time) {
144
+ const newRttSample = Date.now() - time;
145
+ const oldRtt = user.rtt;
146
+
147
+ // сглаживание значения RTT
148
+ // с помощью экспоненциального скользящего среднего (EMA)
149
+ // (предотвращает резкие скачки RTT из-за единичных сетевых флуктуаций)
150
+ user.rtt = oldRtt * (1 - this._alpha) + newRttSample * this._alpha;
151
+
152
+ // если замер RTT превышает установленный порог
153
+ if (this._maxLatency && user.rtt > this._maxLatency) {
154
+ this._callbacks.onKickForMaxLatency(gameId);
155
+ return null; // не возвращается RTT, т.к. игрок будет кикнут
156
+ }
157
+
158
+ // сброс счетчика пропущенных пингов, т.к. игрок ответил
159
+ user.missedPings = 0;
160
+ user.outstandingPings.delete(pingId);
161
+
162
+ return Math.round(newRttSample);
163
+ }
164
+
165
+ // `pong` пришел для пинга, который уже не отслеживается (маловероятно)
166
+ return null;
167
+ }
168
+ }