UNPKG

rock-mod

Version:

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

130 lines (129 loc) 7.38 kB
/// <reference types="@classic-mp/types/client" /> import { NATIVE_DISPATCH } from "./_generated/nativeDispatch"; /** * Реализация `INativeCallerManager` под CCMP — полноценный generic dispatch * GTA V-нативов по hex-хэшу. * * ### Архитектура * * **Таблица hash→method** генерируется на build-time скриптом * `scripts/generate-native-dispatch.mjs` из alloc8or-format nativedb * (`scripts/data/natives.json`, ~6649 натов). Output: * `_generated/nativeDispatch.ts`. Каждая запись — `{ns: "<ccmp-namespace>", * methods: ["<camelMethod1>", "<camelMethod2-fallback>"]}`, где `methods[]` * содержит канонический имя натива + все `old_names` алиасы (нужно потому * что CCMP-биндинги могут использовать либо новое каноническое, либо * легаси-имя в зависимости от момента генерации `@classic-mp/types`). * * **Runtime resolve**: при первом вызове `callNative(hash, ...args)`: * 1. Lookup в `NATIVE_DISPATCH[hash.toLowerCase()]``{ns, methods[]}`. * 2. `ccmp.natives[ns]` — берём namespace-объект. * 3. Перебираем `methods[]` в порядке: для первой существующей функции — * биндим `[nsObj, fn]` в `_resolved` кэш. * 4. Зовём с args через `fn.apply(nsObj, args)`. * 5. Адаптируем return-shape (см. ниже). * * Lookup-кэш `_resolved` — Map с hash-ключом, чтобы не дёргать * `NATIVE_DISPATCH[]` + `_resolve()` на каждый вызов (multiple natives * вызываются на каждый render-tick). * * ### Адаптация return-shape (CCMP → RageMP-конвенция) * * CCMP-нативы для multi-return значений возвращают **объект** с именованными * полями (например, `getGameplayCamCoord()``{x, y, z}`). Геймод (написанный * под RageMP-конвенцию) ожидает **массив-кортеж** (`[x, y, z]`). * * Generic `_adaptResult`: * - `boolean` (top-level) → `0` или `1` (RageMP'шный native-bool протокол: * `mp.game.invoke(boolNative)` возвращает число). * - Object с string-keys → `Object.values(obj)` (порядок гарантирован ES2015 * insertion-order для non-integer keys; CCMP-типы декларируют поля в * каноническом порядке). * - Nested object внутри tuple → рекурсивно тоже `Object.values()` * (для нативов вроде `getShapeTestResult` с nested `endcoords: {x,y,z}`). * - Bool **внутри** объекта/tuple → не конвертим (нужен для tuple-полей * типа `[bool, x, y]` где геймод-тип честно ожидает boolean). * - Primitives / arrays / null → passthrough. * * ### Hot path * * `callNative` вызывается из render-tick consumer'ов: `HudController` * (drawBlip), `CameraIdleController.onInterval`, `VehicleIndicatorService`, * `PolygonZoneRenderer`, `ScriptedMarker`, `ConeDebugRenderer`. После прогрева * `_resolved`-кэша — один Map.get + `fn.apply` + adapt-проход. Адапт — линейный * по числу полей возврата (обычно ≤ 5), стоимость незначительная. * * ### Ошибки * * - **Hash не в DB**: натив отсутствует в nativedb. Throws с подсказкой * "обновите scripts/data/natives.json" — обычно случается для свежих * build-specific натов, не вошедших в last-snapshot базы. * - **Namespace не в `ccmp.natives`**: CCMP-runtime версия не имеет такого * namespace'а. Throws с указанием ns. * - **Все method-кандидаты отсутствуют в namespace'е**: CCMP-биндинг не * экспонирует ни одного из known-имён. Throws с перечислением кандидатов. */ export class CCMPNativeCallerManager { constructor() { this._resolved = new Map(); } callNative(hash, ...args) { const key = hash.toLowerCase(); let resolved = this._resolved.get(key); if (!resolved) { resolved = this._resolve(key, hash); this._resolved.set(key, resolved); } const [nsObj, fn] = resolved; const result = fn.apply(nsObj, args); return CCMPNativeCallerManager._adaptResult(result); } _resolve(key, originalHash) { const entry = NATIVE_DISPATCH[key]; if (!entry) { throw new Error(`CCMPNativeCallerManager.callNative: native hash "${originalHash}" не найден ` + "в nativedb (scripts/data/natives.json). Если натив был добавлен недавно " + "— обновите snapshot из CCMP-проекта и перезапустите " + "`node scripts/generate-native-dispatch.mjs`."); } const natives = ccmp.natives; const nsObj = natives[entry.ns]; if (!nsObj) { throw new Error(`CCMPNativeCallerManager.callNative: native hash "${originalHash}" — namespace ` + `"ccmp.natives.${entry.ns}" отсутствует в runtime. Возможно, версия CCMP ` + "не соответствует версии nativedb."); } for (const method of entry.methods) { const fn = nsObj[method]; if (typeof fn === "function") { return [nsObj, fn]; } } throw new Error(`CCMPNativeCallerManager.callNative: native hash "${originalHash}" — ни один ` + `из method-кандидатов [${entry.methods.join(", ")}] не существует в ` + `ccmp.natives.${entry.ns}. CCMP-биндинг не экспонирует этот натив; ` + "при необходимости — пропатчить @classic-mp/types или маппинг в nativedb."); } /** * Конвертация CCMP object-return → RageMP tuple-return. * См. block-комментарий класса (Адаптация return-shape). */ static _adaptResult(r) { if (typeof r === "boolean") { // Top-level boolean → RageMP-конвенция 0/1. return r ? 1 : 0; } if (r === null || typeof r !== "object" || Array.isArray(r)) { return r; } // Object: flatten в tuple через Object.values, recursively для nested objects. // Booleans внутри объекта НЕ конвертим — геймод-типы могут честно ожидать bool // в multi-return tuple-полях. return Object.values(r).map((v) => { if (v !== null && typeof v === "object" && !Array.isArray(v)) { return Object.values(v); } return v; }); } }