rock-mod
Version:
Rock-Mod is a powerful framework designed for creating and managing mods for Grand Theft Auto (GTA) games.
95 lines (94 loc) • 4.27 kB
JavaScript
/// <reference types="@classic-mp/types/client" />
/**
* Реализация `IKeysManager` поверх нативных CCMP-событий `keyDown`/`keyUp`.
*
* Контрактные тонкости (зеркало RageMP `mp.keys.bind`):
* - `keyHold = false` → handler срабатывает на **нажатие** (`keyDown`).
* - `keyHold = true` → handler срабатывает на **отпускание** (`keyUp`).
* Несмотря на название параметра, это НЕ "только когда зажато",
* это "при отпускании после нажатия". См. RageMP wiki.
*
* Архитектура:
* - Делаем **одну** глобальную подписку на `ccmp.on('keyDown')` и одну
* на `keyUp` в конструкторе. Дальше сами роутим по `key`-коду через
* `Map<key, { down: Set, up: Set }>`. Это намного эффективнее, чем
* регистрировать новый `ccmp.on` на каждый `bind`, и убирает проблему
* отсутствующего у CCMP `ccmp.off` (наш dispatcher остаётся в силе,
* меняется только содержимое внутреннего реестра).
* - Параллельно ведём `_pressedKeys: Set<number>` — текущее состояние,
* чтобы `isDown`/`isUp` отвечали синхронно (у CCMP нет API запроса
* состояния клавиши).
*
* Auto-repeat: если CCMP-клиент эмитит `keyDown` повторно при удержании
* (как WM_KEYDOWN auto-repeat), то и наши `down`-handler'ы будут срабатывать
* повторно — это зеркало RageMP-поведения. Не маскируем намеренно: gamemod-
* консьюмеры писались под такую семантику.
*
* Snapshot при диспатче (`[...binding.down]`) защищает от модификации Set
* из handler'а — `bind`/`unbind` внутри callback'а не ломают текущую итерацию.
*/
export class CCMPKeysManager {
constructor() {
this._pressedKeys = new Set();
this._bindings = new Map();
ccmp.on("keyDown", (key) => {
this._pressedKeys.add(key);
this._dispatch(key, "down");
});
ccmp.on("keyUp", (key) => {
this._pressedKeys.delete(key);
this._dispatch(key, "up");
});
}
isDown(key) {
return this._pressedKeys.has(key);
}
isUp(key) {
return !this._pressedKeys.has(key);
}
bind(key, keyHold, handler) {
let binding = this._bindings.get(key);
if (!binding) {
binding = { down: new Set(), up: new Set() };
this._bindings.set(key, binding);
}
(keyHold ? binding.up : binding.down).add(handler);
}
unbind(key, keyHold, handler) {
const binding = this._bindings.get(key);
if (!binding) {
return;
}
const set = keyHold ? binding.up : binding.down;
if (handler === undefined) {
// RageMP-семантика: без handler — удаляем все подписки на (key, keyHold).
set.clear();
}
else {
set.delete(handler);
}
if (binding.down.size === 0 && binding.up.size === 0) {
this._bindings.delete(key);
}
}
_dispatch(key, phase) {
const binding = this._bindings.get(key);
if (!binding) {
return;
}
const handlers = phase === "down" ? binding.down : binding.up;
if (handlers.size === 0) {
return;
}
// Snapshot: handler может вызвать bind/unbind, мы не должны ломать
// итерацию по Set.
for (const handler of [...handlers]) {
try {
handler();
}
catch (error) {
console.error(`[CCMPKeysManager] ${phase}-handler for key ${key} threw:`, error);
}
}
}
}