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
| Side | Function | Calls export | Returns | Without an inventory |
|---|---|---|---|---|
| both | encore.inventory.isAvailable() | none | boolean | false |
| server | encore.inventory.addItem(source, item, count, metadata?) | AddItem | boolean | false |
| server | encore.inventory.removeItem(source, item, count, metadata?) | RemoveItem | boolean | false |
| server | encore.inventory.canCarry(source, item, count, metadata?) | CanCarryItem | boolean | false |
| server | encore.inventory.getItemCount(source, item, metadata?) | GetItemCount | number | 0 |
| server | encore.inventory.registerUsable(item, handler) | RegisterUsableItem | none | Remembered and applied later |
| client | encore.inventory.getItemCount(item, metadata?) | GetItemCount | number | 0 |
| both | encore.inventory.getItemDefinition(item) | GetItemDefinition | table? | nil |
How results are read
- Boolean results are compared with
== true. An export must return exactlytruefor success;1,'ok'or a table all count as failure. - Counts go through
tonumber, and anything that isn't a number becomes0. - 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 logsen-inventory is not running; item operations will report failureonce. - The export lookup and the call run inside
pcall. A missing or erroring export returns the fallback and logsen-inventory:<Export> failed: <error>once per export name.
Usable items
encore.inventory.registerUsable(item, handler) sets what happens when a player uses an item.
handler(source, item, slot)| Argument | Meaning |
|---|---|
source | The player using the item |
item | The item name |
slot | A 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'sconsumeamount (1 by default for usable items;consume = 0keeps the item). - Handlers are forgotten when the resource that registered them stops.
- Weapons are equipped instead of calling a handler.
-- 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.
metadatafrom a client is untrusted. Build the metadata you store on the server.
-- 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
| Export | Arguments | Must return |
|---|---|---|
AddItem | source, item, count, metadata? | true on success, anything else on failure |
RemoveItem | source, item, count, metadata? | true on success |
CanCarryItem | source, item, count, metadata? | true if the items fit |
GetItemCount | source, item, metadata? | number |
GetItemDefinition | item | { name, label, weight, ... } or nil |
RegisterUsableItem | item, handler | nothing |
Client exports
| Export | Arguments | Must return |
|---|---|---|
GetItemCount | item, metadata? | number |
GetItemDefinition | item | { 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 leastname,countandmetadata. Consume the item only when the handler returnstrue. - Re-registration:
RegisterUsableItemis called again for every item each timeen-inventorystarts. Replace the existing handler rather than adding a second one. - Client counts: implement the client
GetItemCount. en-target'sitemsfilter callsencore.inventory.getItemCounton 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
CanCarryItemandGetItemCountinclude 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.
-- 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)-- 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.
-- en-inventory/fxmanifest.lua (the adapter)
fx_version 'cerulean'
game 'gta5'
lua54 'yes'
server_script 'server.lua'
client_script 'client.lua'
dependencies { 'my_inventory' }-- 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.
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()returnstrueon both server and client.- The console shows no
en-inventory:<Export> failedwarnings. - A usable item consumes one unit when its handler returns
true, and none when it returns anything else. - Restart
en-inventoryand use an item again: it still works. - An en-target option with an
itemsfilter appears only while you carry the item.