Native Inventory is Possible But We Need Structs

So I’ve been investigating on how to add items to the inventory wheel using the original system in RDR2.

This guy figured it out: https://www.rdr2mods.com/forums/topic/2139-addingremoving-items-to-inventory-using-natives

Unfortunately, all the natives involved require pointers to structs which aren’t supported in Lua or safe C#. I tried porting the code but RedM complains about System.Security.VerificationException. I am at wit’s end.

Is there a way in RedM/FiveM, to create objects and pass pointers to those objects to the natives? It would open so many doors to modding.

Is the only solution to make players install a Scripthook plugin?

You can use the Dataview to build pointer in LUA : rdr3_discoveries/AI/EVENTS/dataview_by_Gottfriedleibniz.lua at master · femga/rdr3_discoveries · GitHub

Using that DataView class is particularly tricky in this case. I’m not sure how to port the code. This is the C++ version:

struct sGuid
{
	alignas(8) int data1;
	alignas(8) int data2;
	alignas(8) int data3;
	alignas(8) int data4;
};

struct sSlotInfo
{
	alignas(8) sGuid guid;
	alignas(8) int f_1;
	alignas(8) int f_2;
	alignas(8) int f_3;
	alignas(8) int slotId;
};

With DataView:

local sGuid = DataView.ArrayBuffer(128)
sGuid:SetInt32(0, 0)
sGuid:SetInt32(8, 0)
sGuid:SetInt32(16, 0)
sGuid:SetInt32(24, 0)

local sSlotInfo = DataView.ArrayBuffer(128)
sSlotInfo:SetInt32(0, 0)
sSlotInfo:SetInt32(8, 0) -- How do we put a sGuid buffer in here?
sSlotInfo:SetInt32(16, 0)
sSlotInfo:SetInt32(24, 0)
sSlotInfo:SetInt32(32, 0)

What would we set the second member of sSlotInfo as to be a sGuid?

Currently, no. Anything that is unsafe has to be implemented on the runtime .

For reference this is already something Im working on for mono rt2

Also thank you for linking this research, it is very useful :pray:

1 Like

I figured out how to set the second member in sSlotInfo struct. It’s cumbersome, but you have to set the individual members, int by int, and keep track of the offset for the remaining members. Like so:

local sSlotInfo = DataView.ArrayBuffer(128)
sSlotInfo:SetInt32(0, 0)
    sSlotInfo:SetInt32(0, sGuid:GetInt32(0))
    sSlotInfo:SetInt32(8, sGuid:GetInt32(8))
    sSlotInfo:SetInt32(16, sGuid:GetInt32(16))
    sSlotInfo:SetInt32(24, sGuid:GetInt32(24))
sSlotInfo:SetInt32(16, 0)
sSlotInfo:SetInt32(24, 0)
sSlotInfo:SetInt32(32, 0)

I have now ported TuffyTown’s code into Lua and added some code for weapons which, unfortunately, doesn’t work. It’s annoying because the WEAPON_MELEE_LANTERN should added to your inventory so you can equip after switching weapon.

It’s very cool to see food (e.g., CONSUMABLE_PEACHES_CAN) finally in my inventory in RedM, but when I select and use it, nothing happens. It would be very interesting to see if we can hook an event or someway find out how to detect this action and implement it ourselves. Here’s my Lua port:

function NewGuid()
    local struct = DataView.ArrayBuffer(8 * 4)
    struct:SetInt32(0, 0)
    struct:SetInt32(8, 0)
    struct:SetInt32(16, 0)
    struct:SetInt32(24, 0)
    return struct
end

function NewSlotInfo()
    local guid = NewGuid()

    local struct = DataView.ArrayBuffer(8 * 8)
    -- Begin guid memmber
    struct:SetInt32(0, guid:GetInt32(0))
    struct:SetInt32(8, guid:GetInt32(8))
    struct:SetInt32(16, guid:GetInt32(16))
    struct:SetInt32(24, guid:GetInt32(24))
    -- end guid member

    struct:SetInt32(32, 0) -- int f_1;
    struct:SetInt32(40, 0) -- int f_2;
    struct:SetInt32(48, 0) -- int f_3;
    struct:SetInt32(56, 0) -- int slotId

    struct.GetGuid = function()
        return guid:Buffer()
    end

    struct.SetGuid = function(newGuid)
        struct:SetInt32(0, newGuid:GetInt32(0))
        struct:SetInt32(8, newGuid:GetInt32(8))
        struct:SetInt32(16, newGuid:GetInt32(16))
        struct:SetInt32(24, newGuid:GetInt32(24))
    end

    return struct
end

function NewItemInfo()
    local struct = DataView.ArrayBuffer(8 * 6)
    struct:SetInt32(0, 0)
    struct:SetInt32(8, 0)
    struct:SetInt32(16, 0)
    struct:SetInt32(24, 0)
    struct:SetInt32(32, 0)
    struct:SetInt32(40, 0)
    struct:SetInt32(48, 0)
    return struct
end

function GetPlayerInventoryItemGUID(item, pGuid, slotId)
	local outGuid = NewGuid()
	local result = Citizen.InvokeNative(0x886DFD3E185C8A89, 1, pGuid, item, slotId, outGuid:Buffer(), Citizen.ResultAsInteger());

    if result ~= 1 then
        print("ERROR: Failed to execute INVENTORY_GET_GUID_FROM_ITEMID")
    end

	return outGuid;
end

-- Gets an item's GUID from the inventory
function GetPlayerInventoryGUID()
    return GetPlayerInventoryItemGUID(`CHARACTER`, NewGuid():Buffer(), `SLOTID_NONE`);
end

-- Gets an item's group hash (eInvItemGroup)
function GetItemGroup(item)
	local info = NewItemInfo()

	if false == ItemdatabaseIsKeyValid(item, 0) then
        print("ERROR: ItemdatabaseIsKeyValid is not valid")
		return 0
    end

	if false == Citizen.InvokeNative(0xFE90ABBCBFDC13B2, item, info:Buffer()) then
        print("ERROR: ItemdatabaseFilloutItemInfo returned error")
		return 0
    end

	return info:GetInt32(16);
end

-- Gets an item's slot info data
function GetItemSlotInfo(item)
    local slotInfo = NewSlotInfo()

    slotInfo:SetGuid(GetPlayerInventoryGUID())
    slotInfo:SetInt32(56, `SLOTID_SATCHEL`)

    local group = GetItemGroup(item)
    
    if group == `CLOTHING` then
        if Citizen.InvokeNative(0x780C5B9AE2819807, item, `SLOTID_WARDROBE`) then -- _INVENTORY_FITS_SLOT_ID
            slotInfo:SetGuid( GetPlayerInventoryItemGUID(`WARDROBE`, slotInfo:GetGuid(), `SLOTID_WARDROBE`) )
            slotInfo:SetInt32(56, GetDefaultItemSlotInfo(item, `WARDROBE`))
        else
            slotInfo:SetInt32(56, GetDefaultItemSlotInfo(item, `SLOTID_WARDROBE`))
        end
    elseif group == `WEAPON` then
        if Citizen.InvokeNative(0x780C5B9AE2819807, item, `SLOTID_WEAPON_0`) then
            slotInfo:SetInt32(56, `SLOTID_WEAPON_0`)
        end

        if Citizen.InvokeNative(0x780C5B9AE2819807, item, `SLOTID_WEAPON_1`) then
            slotInfo:SetInt32(56, `SLOTID_WEAPON_1`)
        end
    elseif group == `HORSE` then
        slotInfo:SetInt32(56, `SLOTID_ACTIVE_HORSE`)
    elseif group == `EMOTE` then
    elseif group == `UPGRADE` then
        if Citizen.InvokeNative(0x780C5B9AE2819807, item, `SLOTID_UPGRADE`) then 
            slotInfo:SetInt32(56, `SLOTID_UPGRADE`)
        end
    else
        if Citizen.InvokeNative(0x780C5B9AE2819807, item, `SLOTID_SATCHEL`) then 
            slotInfo:SetInt32(56, `SLOTID_SATCHEL`)
        elseif Citizen.InvokeNative(0x780C5B9AE2819807, item, `SLOTID_WARDROBE`) then 
            slotInfo:SetInt32(56, `SLOTID_WARDROBE`)
        else
            slotInfo:SetInt32(56, GetDefaultItemSlotInfo(item, `CHARACTER`))
        end
    end

    return slotInfo
end

-- Adds an item to the player inventory via GUID
function AddItemWithGUID(item, guid, slotInfo, quantity, addReason)

    if false == Citizen.InvokeNative(0xB881CA836CC4B6D4, slotInfo:GetGuid()) then
        print("INVALID GUID")
        return false
    end

    if false == Citizen.InvokeNative(0xCB5D11F9508A928D, 1, guid:Buffer(), slotInfo:GetGuid(), item, slotInfo:GetInt32(56), quantity, addReason) then
        print("FAILED TO ADD TO INVENTORY")
        return false
    end

	return true;
end

-- Adds an item to the player inventory via hash
-- This is the main function you will be calling to add items to your inventory
function AddItemToInventory(item, quantity)
	local slotInfo = GetItemSlotInfo(item);
	local guid = GetPlayerInventoryItemGUID(item, slotInfo:GetGuid(), slotInfo:GetInt32(56));
	return AddItemWithGUID(item, guid, slotInfo, quantity, `ADD_REASON_DEFAULT`);
end

Example use:

AddItemToInventory(`CONSUMABLE_PEACHES_CAN`, 1)

I have also been heavily researching into this and had actually successfully added stuff to the user’s inventory, tho definitely needs more work. The items do not seem functional, so either they’re not valid items or the item’s usages have to be manually scripted. Not sure why the tonic was put in 3 different categories, probably something missing. Here’s a screenshot:

Here’s the test script I used to experiment with as well. Hope it helps out. (assuming you already know about dataview)

RegisterCommand("testiteminfo", function()
  local item = "CONSUMABLE_POTENT_TONIC"
  local itemhash = GetHashKey(item)
  AddItemToInventory(itemhash, 1)
end)

function GetPlayerInventoryItemGUID(item, guid, slotId) 
  local outGuid = DataView.ArrayBuffer(8 * 13)
  Citizen.InvokeNative(0x886DFD3E185C8A89, 1, guid:Buffer(), item, slotId, outGuid:Buffer())
  return outGuid
end

function GetPlayerInventoryGUID()
  return GetPlayerInventoryItemGUID(GetHashKey("CHARACTER"), DataView.ArrayBuffer(8 * 13), GetHashKey("SLOTID_NONE"))
end

function GetItemGroup(item)
  if not Citizen.InvokeNative(0x6D5D51B188333FD1, item, 0) then
    return 0
  end
  local outItem = DataView.ArrayBuffer(8 * 13)
  local retval = Citizen.InvokeNative(0xFE90ABBCBFDC13B2, item, outItem:Buffer())
  if not retval then
    return 0
  end
  print(outItem:GetUint32(0), outItem:GetInt32(8), outItem:GetUint32(16))
  -- local hash = GetUint32(0)
  -- local unknown1 = GetUint32(8)
  -- local group = GetUint32(16)
  return outItem:GetUint32(16)
end

function GetItemSlotInfo(item)
  local guid = GetPlayerInventoryGUID()
  guid:SetUint32(32, GetHashKey("SLOTID_SATCHEL"))
  local group = GetItemGroup(item)
  if group == 3257429761 then
    if not Citizen.InvokeNative(0x780C5B9AE2819807, item, GetHashKey("SLOTID_WARDROBE")) then
      guid = GetPlayerInventoryItemGUID(GetHashKey("WARDROBE"), guid, GetHashKey("SLOTID_WARDROBE"))
      guid:SetInt32(32, Citizen.InvokeNative(0x6452B1D357D81742, item, GetHashKey("WARDROBE")))
    else
      guid:SetUint32(32, GetHashKey("SLOTID_WARDROBE"))
    end
  elseif group == 2510745927 then
    guid:SetUint32(32, GetHashKey("SLOTID_ACTIVE_HORSE"))
  elseif group == 2163970765 then
    if Citizen.InvokeNative(0x780C5B9AE2819807, item, GetHashKey("SLOTID_UPGRADE")) then
      guid:SetUint32(32, GetHashKey("SLOTID_UPGRADE"))
    end
  else
    if Citizen.InvokeNative(0x780C5B9AE2819807, item, GetHashKey("SLOTID_SATCHEL")) then
      guid:SetUint32(32, GetHashKey("SLOTID_SATCHEL"))
    elseif Citizen.InvokeNative(0x780C5B9AE2819807, item, GetHashKey("SLOTID_WARDROBE")) then
      guid:SetUint32(32, GetHashKey("SLOTID_WARDROBE"))
    else
      guid:SetUint32(32, Citizen.InvokeNative(0x6452B1D357D81742, item, GetHashKey("CHARACTER")))
    end
  end
  return guid
end

function AddItemWithGUID(item, guid, slotinfo, slotId, quantity, addreason)
  local retval = Citizen.InvokeNative(0xCB5D11F9508A928D, 1, guid:Buffer(), slotinfo:Buffer(), item, slotId, quantity, addreason)
  if not retval then
    print("^1 Could not add item")
    return false
  end
  return true
end

function AddItemToInventory(item, quantity)
  local guid_1 = GetItemSlotInfo(item)
  local guid = GetPlayerInventoryItemGUID(item, guid_1, guid_1:GetInt32(32))
  return AddItemWithGUID(item, guid, guid_1, guid_1:GetInt32(32), quantity, GetHashKey("ADD_REASON_DEFAULT"))
end

Edit: I didn’t see the guy’s reply above before I posted this.

1 Like

I have now ported TuffyTown’s code into Lua and added some code for weapons which, unfortunately, doesn’t work. It’s annoying because the WEAPON_MELEE_LANTERN should added to your inventory so you can equip after switching weapon.

The weapon stuff you posted isn’t complete, the game code calls down into another function to handled CARRIED_WEAPONS with SLOTID_CARRIED_WEAPONS and sets the GUID

Huh, func_57? I must’ve missed that due to coding at 2 AM :upside_down_face:

So you can successfully acquire the CARRIED_WEAPONS inventory GUID the following way:

local carriedWeaponsInventoryGUID = GetPlayerInventoryItemGUID(`CARRIED_WEAPONS`, GetPlayerInventoryGUID():Buffer(), `SLOTID_CARRIED_WEAPONS`)

I know it’s valid because INVENTORY_IS_GUID_VALID returns true.

However, I am unable to use INVENTORY_GET_GUID_FROM_ITEMID using that inventory GUID:

local itemGuid = NewStruct(5)
local result = Citizen.InvokeNative(0x886DFD3E185C8A89, 1, carriedWeaponsInventoryGUID:Buffer(), `WEAPON_REVOLVER_CATTLEMAN`, `SLOTID_WEAPON_0`, itemGuid:Buffer(), Citizen.ResultAsInteger());

INVENTORY_IS_GUID_VALID yields zero at this point for item guid. (I’m using SLOTID_WEAPON_0 for the revolver because INVENTORY_FITS_SLOT_ID says it fits)

Any Updates? I would be really interested in this!

Yeah, now I just add items using an empty GUID. Also, weapons were getting equipped on the horse of all places, and that’s why I couldn’t immediately see them.

I ended up with a working solution and added it to my standalone wild framework. You can see it in use here: wild/wild-satchel/client/cl_inventory.lua at 9235aaa39696691ff26977ff1d2c18fe67971ef5 · aaron1a12/wild · GitHub

3 Likes

Saw your repo this weekend and I’m intrigued that someone actually went ahead and implemented a seemingly full inventory management system using native inventory functionality.

Does it work well? I had a similar prototype but gave up in the end due to all the bits and pieces you need to account for. Yours seems way more thought through and seemingly feature complete?

Would love to get some insights / experience and potentially open todos (I saw you had placeholders for weapon slot detection and horse stuff).

With this as a base, the native UIApp satchel should be achievable?

I used this a TON during my research on inventory UIAPP: