UNPKG

rock-mod

Version:

Rock-Mod is a powerful framework designed for creating and managing mods for Grand Theft Auto (GTA) games.

178 lines (177 loc) 9.73 kB
import { CCMPInProcessEmitter } from "./CCMPInProcessEmitter"; /** * Реализация `IEventsManager` поверх нативного CCMP клиента. * * Важные особенности CCMP, продиктовавшие дизайн: * * 1. У `ccmp.on(name, cb)` **нет** парного `ccmp.off`. Поэтому unsubscribe * реализован через собственный реестр: на каждое имя регистрируется ровно * один диспетчер в `ccmp`, который проходит по нашему `Set` подписчиков. * `off` лишь удаляет колбэк из `Set`, диспетчер на стороне CCMP остаётся * висеть (становится no-op). * * 2. `ccmp.emitServer(event, data)` принимает **одно** значение `data`. Наш * контракт `emitServer(name, ...args)` — variadic. Чтобы не терять * позиционные аргументы, всегда отправляем `args` массивом, а на приёме * разворачиваем обратно. Это совпадает с тем, как сервер уже шлёт клиенту * (`ccmpPlayer.emit(name, args)` в `CCMPEventsManager` на сервере). * * 3. Локальной in-process шины событий (аналог `mp.events.call`) у CCMP нет — * под `emitInternal` подкладываем `CCMPInProcessEmitter`. Однако `onInternal` * дополнительно подписывает handler ещё и на нативный `ccmp.on(name, ...)`, * потому что геймод использует `events.onInternal` как канал для UI-ingress * (см. `rock-mod-event-emitter.adapter.ts:61-66` в геймоде: `registerUI()` * делегирует в `events.onInternal`). Под RageMP такого дубля не нужно, * потому что `mp.events.add` естественно ловит и `mp.events.call`, и * CEF-выпуски `mp.trigger`. Побочный эффект: server/system события с тем * же именем тоже долетят до internal-handler'а — на практике конфликтов * нет, потому что namespace'ы геймода различаются (`rm::*` server, * `api:*` UI, `player:*`/`vehicle:*`/etc. internal). * * 4. `IClientEvents` (для `onRaw`/`offRaw`) — RageMP-specific глобальный * тип. Под CCMP осмысленно регистрировать `ccmp.on('playerConnected', ...)` * и другие билтины, но строго типизировать их через `IClientEvents` нельзя * — используем `@ts-expect-error` по аналогии с серверным `CCMPEventsManager`. */ export class CCMPEventsManager { constructor() { /** Реестр всех серверных/raw/generic подписчиков. */ this._externalListeners = new Map(); /** Имена, для которых уже зарегистрирован диспетчер в нативном `ccmp`. */ this._dispatched = new Set(); /** Локальная in-process шина — питает `onInternal`/`emitInternal`. */ this._internalEmitter = new CCMPInProcessEmitter(); } // -- Raw (CCMP builtin) --------------------------------------------------- onRaw(events) { // IClientEvents is RageMP-specific; под CCMP принимаем тот же call-shape, // но привязываем через свой реестр диспетчеров. for (const eventName of Object.keys(events)) { const handler = events[eventName]; if (!handler) { continue; } this._registerExternal(eventName, handler); } } offRaw(eventName, listener) { this._unregisterExternal(eventName, listener); } // -- Internal (in-process) ------------------------------------------------ onInternal(events) { for (const eventName of Object.keys(events)) { const handler = events[eventName]; if (!handler) { continue; } // 1) Локальная in-process шина — питает `emitInternal` от client-кода. this._internalEmitter.on(eventName, handler); // 2) UI ingress. Геймод-адаптер `registerUI()` маппит UI-events через // `events.onInternal`. Под CCMP UI шлёт `window.ccmp.emitClient(name, payload)`, // которое приходит на клиент через `ccmp.on(name, handler)`. Подписываем // тот же handler на нативный канал через общий `_registerExternal`-реестр // (он управляет single-dispatcher'ом и Array.isArray-unwrap'ом). this._registerExternal(eventName, handler); } } offInternal(eventName, listener) { this._internalEmitter.off(eventName, listener); this._unregisterExternal(eventName, listener); } emitInternal(eventName, ...args) { this._internalEmitter.emit(eventName, ...args); } /** * Sticky-вариант `emitInternal`. Кэширует последнее значение, чтобы поздние * подписчики (`onInternal`) получили его сразу при подписке. Использовать * для one-shot lifecycle-событий (`rm::playerReady` и т.п.), где гонка между * "событие случилось" и "подписчик зарегистрирован" критична. * * См. `CCMPInProcessEmitter.emitSticky` для подробностей семантики. */ emitInternalSticky(eventName, ...args) { this._internalEmitter.emitSticky(eventName, ...args); } /** * Очищает sticky-кэш для конкретного internal-события. Использовать на * disconnect/reset чтобы новые подписчики не получали устаревший local- * player-stub после реконнекта. */ clearInternalSticky(eventName) { this._internalEmitter.clearSticky(eventName); } // -- Server <-> Client ---------------------------------------------------- onServer(events) { for (const eventName of Object.keys(events)) { const handler = events[eventName]; if (!handler) { continue; } this._registerExternal(eventName, handler); } } offServer(eventName, listener) { this._unregisterExternal(eventName, listener); } emitServer(eventName, ...args) { // Заворачиваем variadic args в массив — соответствует контракту приёма // на сервере (`CCMPEventsManager.onClient` разворачивает обратно). ccmp.emitServer(eventName, args); } // -- Generic escape hatch ------------------------------------------------- register(event, listener) { this._internalEmitter.on(event, listener); this._registerExternal(event, listener); } unregister(event) { this._internalEmitter.off(event); this._unregisterExternal(event); } // -- Implementation details ----------------------------------------------- _registerExternal(event, listener) { let bucket = this._externalListeners.get(event); if (!bucket) { bucket = new Set(); this._externalListeners.set(event, bucket); } bucket.add(listener); if (!this._dispatched.has(event)) { this._dispatched.add(event); ccmp.on(event, (payload) => { const handlers = this._externalListeners.get(event); if (!handlers || handlers.size === 0) { return; } // Сервер заворачивает variadic args в массив через // `ccmpPlayer.emit(name, args)`. Однопараметрические/нативные события // (билтины CCMP типа `playerConnected`) приходят как один объект — // их прокидываем как единственный аргумент. const args = Array.isArray(payload) ? payload : [payload]; for (const handler of [...handlers]) { try { handler(...args); } catch (error) { console.error(`[CCMPEventsManager] handler "${event}" failed:`, error); } } }); } } _unregisterExternal(event, listener) { const bucket = this._externalListeners.get(event); if (!bucket) { return; } if (listener) { bucket.delete(listener); if (bucket.size === 0) { this._externalListeners.delete(event); } return; } this._externalListeners.delete(event); // Нативный `ccmp.on`-диспетчер остаётся висеть — у CCMP нет `off`. // Когда в реестре пусто, он просто ничего не делает. } }