Reading RDR3 AI events (skinning/loot) from TypeScript: poll the event queue + marshal GET_EVENT_DATA with a JS ArrayBuffer
Context
RDR3 AI/script events (EVENT_LOOT, EVENT_LOOT_COMPLETE, skinning, etc.) are NOT delivered through FiveM's gameEventTriggered / CEvent* handler — that's a GTA5/FiveM mechanism. Hooking on('gameEventTriggered', ...) for these in RedM gives ZERO output (verified: a skin produced nothing). RDR3 instead exposes a polled event QUEUE, the same one jo_libs modules/game-events/g_client.lua wraps in Lua. This finding is the TypeScript/JS port, because the buffer-marshalling step is the part the JS invoke guide flags as "awkward" — and it turns out to work cleanly.
The recipe (poll every frame, group 0 = SCRIPT_EVENT_QUEUE_AI)
const GET_NUMBER_OF_EVENTS = "0x5CE8DE5909565748"; // (group) -> int
const GET_EVENT_AT_INDEX = "0xA85E614430EFF816"; // (group, i) -> event-name Hash
const GET_EVENT_DATA = "0x57EC5FA4D4D6AFCA"; // (group, i, buf, fieldCount) -> BOOL
setTick(() => {
const count = Citizen.invokeNative(GET_NUMBER_OF_EVENTS, 0, Citizen.resultAsInteger());
for (let i = 0; i < count; i++) {
const hash = Citizen.invokeNative(GET_EVENT_AT_INDEX, 0, i, Citizen.resultAsInteger()) >>> 0;
if (hash !== (GetHashKey("EVENT_LOOT_COMPLETE") >>> 0)) continue;
const size = 3; // field COUNT from jo_libs data.lua
const buf = new ArrayBuffer(8 * size);
const ok = Citizen.invokeNative(GET_EVENT_DATA, 0, i, new Uint8Array(buf), size, Citizen.resultAsInteger());
if (!ok) continue;
const view = new DataView(buf);
const fields = [];
for (let f = 0; f < size; f++) fields.push(view.getInt32(8 * f, true)); // 8-byte stride, little-endian
// EVENT_LOOT_COMPLETE -> [initiator_entity, target_entity, is_loot_success]
}
});
The traps (all verified live on build 1491)
- Match events by HASH from
GET_EVENT_AT_INDEX, not by a string name. Compare againstGetHashKey("EVENT_LOOT_COMPLETE"). GET_EVENT_DATA's last arg is the FIELD COUNT (RAGE SIZE_OF = bytes/8), NOT a byte length. EVENT_LOOT_COMPLETE = 3, EVENT_LOOT = 36. Get these from jo_libsmodules/game-events/data.lua.- Buffer is 8-byte-aligned per field, zero-based, LITTLE-endian. Read each Int32 at
view.getInt32(8 * fieldIndex, true). Even BOOL/Int32 fields occupy a full 8 bytes. - JS marshalling works: pass
new Uint8Array(buf)as the pointer arg and read back from the sameArrayBufferviaDataView. Verified decode: a skin produced[279810, 9614082, 1]where field 1 (target_entity) exactly matched the carcass entity handle and field 2 (is_loot_success) = 1. Citizen.invokeNativeis lowercase-i in JS (capital-I is the Lua spelling and throws "not a function").
Use for hunting/skinning
EVENT_LOOT (size 36) fires at skin START; field 28 = looted_entity_model (the model hash) while the carcass exists. EVENT_LOOT_COMPLETE (size 3) fires at the end. Easiest: on EVENT_LOOT_COMPLETE with is_loot_success=1, GetEntityModel(target_entity) (the carcass usually still exists) to identify the animal, then grant items server-side.
Linked natives: GET_NUMBER_OF_EVENTS 0x5CE8DE5909565748, GET_EVENT_AT_INDEX 0xA85E614430EFF816, GET_EVENT_DATA 0x57EC5FA4D4D6AFCA. Related: redm-mcp learning #48 (DataView 8-byte-stride marshalling, Lua side).
Linked natives
GET_EVENT_AT_INDEX(0xA85E614430EFF816)GET_EVENT_DATA(0x57EC5FA4D4D6AFCA)GET_NUMBER_OF_EVENTS(0x5CE8DE5909565748)
Tags: game-events, skinning, hunting, typescript, invokenative, dataview, event-queue, rdr3
Category: natives
Source: scandi-hunting-build
Created: 2026-06-04T12:11:59.902Z