UNPKG

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
/// <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); } } } }