EndCore Framework

Inventory interface

The encore.inventory API every EndCore resource uses for items, the exact contract a replacement inventory must implement, and the two ways to plug one in.

EndCore resources never call an inventory resource directly. They add, remove, count and register usable items through encore.inventory, and the library forwards each call to a resource named en-inventory. EndCore ships en-inventory, but because everything goes through this interface you can replace it with your own inventory without touching a single other resource.

When no inventory is running, every call reports failure instead of erroring, so resources degrade gracefully.

Library API

SideFunctionCalls exportReturnsWithout an inventory
bothencore.inventory.isAvailable()nonebooleanfalse
serverencore.inventory.addItem(source, item, count, metadata?)AddItembooleanfalse
serverencore.inventory.removeItem(source, item, count, metadata?)RemoveItembooleanfalse
serverencore.inventory.canCarry(source, item, count, metadata?)CanCarryItembooleanfalse
serverencore.inventory.getItemCount(source, item, metadata?)GetItemCountnumber0
serverencore.inventory.registerUsable(item, handler)RegisterUsableItemnoneRemembered and applied later
clientencore.inventory.getItemCount(item, metadata?)GetItemCountnumber0
bothencore.inventory.getItemDefinition(item)GetItemDefinitiontable?nil

How results are read

  • Boolean results are compared with == true. An export must return exactly true for success; 1, 'ok' or a table all count as failure.
  • Counts go through tonumber, and anything that isn't a number becomes 0.
  • A definition is only passed through if the export returns a table. Anything else becomes nil.

When calls fail

  • Every call first checks GetResourceState('en-inventory') == 'started'. If it isn't, the call returns the fallback and logs en-inventory is not running; item operations will report failure once.
  • The export lookup and the call run inside pcall. A missing or erroring export returns the fallback and logs en-inventory:<Export> failed: <error> once per export name.

Usable items

encore.inventory.registerUsable(item, handler) sets what happens when a player uses an item.

lua
handler(source, item, slot)
ArgumentMeaning
sourceThe player using the item
itemThe item name
slotA snapshot of the used stack. en-inventory passes { id, name, count, metadata }

The library remembers every handler in your resource. It applies the handler immediately if en-inventory is started, and applies every remembered handler again each time en-inventory starts. Registration order doesn't matter, and restarting the inventory never leaves items unusable.

How en-inventory treats handlers:

  • If the handler returns true, en-inventory consumes the item definition's consume amount (1 by default for usable items; consume = 0 keeps the item).
  • Handlers are forgotten when the resource that registered them stops.
  • Weapons are equipped instead of calling a handler.
lua
-- server
encore.inventory.registerUsable('bandage', function(source, item, slot)
    local ok = encore.progressOnClient and false or encore.callback.await('my-medical:applyBandage', source)
    return ok == true   -- only consume a bandage if the client finished bandaging
end)

Security

  • Items are only added and removed on the server. The client can only read counts and definitions.
  • Never give items straight from a client event. Check what the player did first: distance, cooldowns, and that they had the materials.
  • A client-side count is fine for showing or hiding an option. Check the count again on the server before acting on it.
  • metadata from a client is untrusted. Build the metadata you store on the server.
lua
-- server: a safe loot handout
RegisterNetEvent('my-loot:search', function(containerId)
    local src = source
    local container = type(containerId) == 'string' and Containers[containerId]
    if not container or container.searched then return end

    if #(GetEntityCoords(GetPlayerPed(src)) - container.coords) > 3.0 then return end

    container.searched = true
    if encore.inventory.canCarry(src, 'scrap', 3) then
        encore.inventory.addItem(src, 'scrap', 3)
    else
        encore.notify(src, { description = 'Your pack is full', type = 'error' })
    end
end)

Replacing the inventory

The contract

The library looks for exports on a resource named exactly en-inventory. There is no convar or setting to point it at another name. Whatever answers must provide these exports with these signatures:

Server exports

ExportArgumentsMust return
AddItemsource, item, count, metadata?true on success, anything else on failure
RemoveItemsource, item, count, metadata?true on success
CanCarryItemsource, item, count, metadata?true if the items fit
GetItemCountsource, item, metadata?number
GetItemDefinitionitem{ name, label, weight, ... } or nil
RegisterUsableItemitem, handlernothing

Client exports

ExportArgumentsMust return
GetItemCountitem, metadata?number
GetItemDefinitionitem{ name, label, weight, ... } or nil

Behaviour to match so EndCore resources work as expected:

  • Usable handlers: call handler(source, item, slot) when an item is used, passing a slot table with at least name, count and metadata. Consume the item only when the handler returns true.
  • Re-registration: RegisterUsableItem is called again for every item each time en-inventory starts. Replace the existing handler rather than adding a second one.
  • Client counts: implement the client GetItemCount. en-target's items filter calls encore.inventory.getItemCount on the client, so item-gated interactions depend on it.
  • Carrying: in en-inventory "the player" means their pockets plus a worn bag, checked in that order. If your inventory has bags or containers, decide what CanCarryItem and GetItemCount include and keep it consistent.

Option 1: ship your inventory as en-inventory

The direct route: your inventory resource is named en-inventory and exports the contract itself. Stop and remove the shipped en-inventory first.

lua
-- en-inventory/server/contract.lua
exports('AddItem', function(source, item, count, metadata)
    return MyInv.give(source, item, count or 1, metadata) == true
end)

exports('RemoveItem', function(source, item, count, metadata)
    return MyInv.take(source, item, count or 1, metadata) == true
end)

exports('CanCarryItem', function(source, item, count, metadata)
    return MyInv.fits(source, item, count or 1, metadata) == true
end)

exports('GetItemCount', function(source, item, metadata)
    return MyInv.count(source, item, metadata) or 0
end)

exports('GetItemDefinition', function(item)
    local def = MyInv.items[item]
    return def and { name = item, label = def.label, weight = def.weight } or nil
end)

exports('RegisterUsableItem', function(item, handler)
    MyInv.usable[item] = handler   -- call handler(source, item, slot); consume if it returns true
end)
lua
-- en-inventory/client/contract.lua
exports('GetItemCount', function(item, metadata)
    return MyInvClient.count(item, metadata) or 0
end)

exports('GetItemDefinition', function(item)
    return MyInvClient.items[item]
end)

Option 2: keep your inventory's name and add an adapter

If your inventory must keep its own resource name, for example because other scripts already call it, run a small adapter resource named en-inventory next to it. The adapter implements the contract by translating each call into your inventory's own API.

lua
-- en-inventory/fxmanifest.lua (the adapter)
fx_version 'cerulean'
game 'gta5'
lua54 'yes'

server_script 'server.lua'
client_script 'client.lua'

dependencies { 'my_inventory' }
lua
-- en-inventory/server.lua
local INV = 'my_inventory'
local usable = {}

exports('AddItem', function(source, item, count, metadata)
    return exports[INV]:GiveItem(source, item, count or 1, metadata) == true
end)

exports('RemoveItem', function(source, item, count, metadata)
    return exports[INV]:TakeItem(source, item, count or 1, metadata) == true
end)

exports('CanCarryItem', function(source, item, count, metadata)
    return exports[INV]:CanFit(source, item, count or 1, metadata) == true
end)

exports('GetItemCount', function(source, item, metadata)
    return tonumber(exports[INV]:Count(source, item, metadata)) or 0
end)

exports('GetItemDefinition', function(item)
    return exports[INV]:GetItem(item)
end)

local function applyUsable(item, handler)
    exports[INV]:SetUseHandler(item, function(source, stack)
        return handler(source, item, { name = item, count = stack.count, metadata = stack.metadata }) == true
    end)
end

exports('RegisterUsableItem', function(item, handler)
    usable[item] = handler
    applyUsable(item, handler)
end)

-- The library only re-registers when en-inventory starts, so replay
-- handlers yourself if your inventory restarts on its own.
AddEventHandler('onResourceStart', function(resource)
    if resource ~= INV then return end
    for item, handler in pairs(usable) do applyUsable(item, handler) end
end)

The export names on my_inventory above stand in for whatever your inventory actually provides.

You can also answer the contract from inside your own inventory with encore.provideExport, for example encore.provideExport('en-inventory', 'AddItem', fn). That event-based approach doesn't remove the need for a started resource named en-inventory, though.

Warning

Every library call checks GetResourceState('en-inventory') == 'started' first. With provideExport, a resource named en-inventory must still be started, even an empty one, or the library returns the fallback without ever asking. Don't answer the same export name from two places, and never run the shipped en-inventory alongside your adapter.

Checking your replacement

  • encore.inventory.isAvailable() returns true on both server and client.
  • The console shows no en-inventory:<Export> failed warnings.
  • A usable item consumes one unit when its handler returns true, and none when it returns anything else.
  • Restart en-inventory and use an item again: it still works.
  • An en-target option with an items filter appears only while you carry the item.