Knowledge from the fieldRedM

Reading RDR3 AI events (skinning/loot) from TypeScript: poll the event queue + marshal GET_EVENT_DATA with a JS ArrayBuffer

learning:81

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 against GetHashKey("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_libs modules/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 same ArrayBuffer via DataView. 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.invokeNative is 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

Back to documentation