EndCore Framework

en-inventory

The spatial grid inventory with rotatable items, hotbar, backpacks, trunks, stashes, ground bags, weapon ammo and persistent death bags.

en-inventory is a spatial grid inventory in the style of Resident Evil or DayZ. Every container is a grid of cells, and every item takes up a w x h footprint that you can rotate. Players carry an 8x6 grid with a 30 kg base limit and a 5-slot hotbar, and can wear a bag (a backpack or duffel) that adds its own grid underneath.

TAB opens your inventory next to whatever is nearby: a glovebox when you are in a vehicle, a trunk when you stand behind an unlocked vehicle, a stash, a ground bag, or an empty "ground" grid to drop items onto. Weapons are items that carry their own magazine ammo and durability. When you die, part of your gear spills into a persistent death bag marked on your map.

At a glance

Depends on/onesync, oxmysql, en-core, en-ui
Optionalen-clothing (worn slots and bags), en-target (trunk, bag and stash options), en-skills (carry bonus)
Start afteroxmysql, en-core, en-ui. Resources that register usable items re-register automatically when the inventory starts
Database tablesencore_inventories, encore_death_bags (created automatically)
Config filesconfig/shared.lua (sent to clients), config/server.lua (server only), data/items.lua
Key bindsen_inventory_open (TAB), en_inventory_reload (R), en_inventory_hotbar_1 to en_inventory_hotbar_5 (1-5)

How it works

The grid

  • An item instance is { id, name, count, x, y, rotated, metadata }. x and y are the top-left cell, starting at 1. A stack is one instance with count above 1, using the same footprint.
  • Rotating swaps an item's width and height.
  • When space is needed, rows are scanned top to bottom, left to right, trying the item upright first and then rotated.
  • Adding items first tops up existing stacks with identical metadata, then places new stacks. AddItem and CanCarryItem use the same plan, so they always agree.
  • Weight is weight x count, plus metadata.contentWeight for bags that hold items.
  • The server is authoritative on every move. The client uses the same maths only to preview drags.

A player's max weight is Config.player.maxWeight plus Player(src).state.carryBonus (from en-skills) plus Player(src).state.bagCapacity (from en-clothing). Changes to those state bags update the limit live.

Containers

ContainerIdNotes
Stashstash:<id>Fixed world stashes from config, or registered by other resources
Trunktrunk:<plate>Size by vehicle class; persists only when Entity(vehicle).state.owned == true
Gloveboxglovebox:<plate>You must be in the vehicle; same persistence rule as trunks
Ground bagbag:<random>Client-local props within 60 m; despawn after 30 minutes untouched
Death bagdeath:<hex>Persists across restarts until emptied or expired

Every server action re-checks that the player can reach the container: distance (with 1.5 m slack for drops and stashes), group rules, canOpen, and being inside the vehicle for gloveboxes.

Backpacks

The item in en-clothing's bag slot, if its definition has a container, gets its own grid shown under the player's grid. The storage id is kept in the worn item's metadata.container, and the weight of its contents is written to metadata.contentWeight, so the bag stays heavy when you take it off. A bag's contents are only reachable while it is worn, and bags never go inside bags.

Weapons

Weapons are equipped from the inventory or hotbar. Their rounds live in metadata.ammo, synced every 5 seconds and after firing. R reloads from matching ammo items (player grid first, then bag). Each shot wears the weapon down, and a weapon at 0 durability is holstered. GTA's weapon wheel and weapon number keys are disabled by default.

Death bags

When en-core reports a death (encore:server:playerDied, with a 10 second debounce):

  1. A share of carried cash (cashPercent) always drops.
  2. Each stack rolls to drop: the item's deathDrop, else its category chance, else itemChance.
  3. Each worn clothing piece rolls clothingChance. A worn bag rolls bagChance and takes its contents with it.

The bag persists across restarts until it is emptied or reaches lifetimeHours. The owner gets a map blip for their newest bag.

Configuration

config/shared.lua

KeyDefaultWhat it does
Config.player.width / height8 / 6Player grid size in cells
Config.player.maxWeight30.0Base carry weight (kg)
Config.player.hotbarSlots5Hotbar slots (keys 1-5)
Config.drop8x6, maxWeight 500.0, prop 'prop_cs_heist_bag_02', range 2.5, renderDistance 60.0Ground bags
Config.death.prop'prop_cs_heist_bag_01'Death bag prop
Config.death.blip{ sprite = 310, colour = 1, scale = 0.9, label = 'Your belongings' }Owner's map marker
Config.vehicles.trunkDistance3.5How close to the trunk you must stand
Config.vehicles.trunk[class]see belowTrunk size by vehicle class, { w, h, maxWeight } or false
Config.vehicles.glovebox{ 4, 2, 5.0 }Glovebox size and weight
Config.keys.inventory / reload'TAB' / 'R'Default keys
Config.weapons.disableWheeltrueBlock the GTA weapon wheel and weapon number keys
Config.weapons.ammoSyncInterval5000 msHow often the client reports rounds fired
Config.weapons.wearPerShot0.05 (not in the file by default)Durability lost per shot
Config.imagestrueUse html/images/<item>.png; otherwise a category glyph

Default trunk sizes: default = { 6, 4, 50.0 }, compacts { 5, 3, 35 }, SUVs { 8, 5, 90 }, vans { 10, 6, 220 }, commercial { 12, 8, 400 }. Motorcycles, cycles and trains are false (no trunk).

config/server.lua

KeyDefaultWhat it does
ServerConfig.autosaveInterval60000 msChanged inventories are flushed to the database
ServerConfig.dropLifetime30 * 60 sUntouched ground bags despawn
ServerConfig.death.enabledtrueDeath bags on or off
ServerConfig.death.cashPercent0.5Share of carried cash that always drops
ServerConfig.death.itemChance0.4Drop chance per stack
ServerConfig.death.categories{ weapon = 0.6, ammo = 0.5, key = 0.0 }Per-category overrides
ServerConfig.death.clothingChance0.25Drop chance per worn clothing piece
ServerConfig.death.bagChance0.5Drop chance for a worn bag (with its contents)
ServerConfig.death.lifetimeHours48Death bag expiry; 0 keeps bags forever
ServerConfig.death.bag{ width = 10, height = 8 }Death bag grid; grows by 4 rows (up to 12 times) if needed
ServerConfig.adminPrincipal'group.admin'ACE principal for the admin commands
ServerConfig.stashesone example, burton_lockerFixed world stashes

A stash entry takes id, label, coords, distance, width, height, maxWeight, plus optional personal, shared and groups. groups accepts:

ValueWho can open
{ medic = 0 }Job medic, any grade
{ group = 0 }Anyone in a player group
{ ['group:12'] = 2 }Player group 12, grade 2 or higher
lua
-- config/server.lua
ServerConfig.death.cashPercent = 1.0   -- lose all carried cash
ServerConfig.death.lifetimeHours = 6

ServerConfig.stashes = {
    {
        id = 'burton_locker', label = 'Checkpoint Locker',
        coords = vec3(-255.1, -299.4, 21.6), distance = 1.5,
        width = 10, height = 8, maxWeight = 200.0,
        personal = true,
    },
    {
        id = 'clinic_supplies', label = 'Clinic Supplies',
        coords = vec3(1839.2, 3672.9, 34.3), distance = 2.0,
        width = 8, height = 6, maxWeight = 120.0,
        groups = { medic = 0 },
    },
}

Item definitions

data/items.lua returns a table keyed by item name. Weapon items use the weapon name, such as WEAPON_PISTOL. Cash is an item too: cash, 1x1, weight 0, stacks to 1,000,000.

FieldWhat it does
label, descriptionDisplay text
categoryfood, drink, medical, weapon, ammo, tool, material, clothing, equipment, misc, key or currency
size{ w, h } footprint in cells
weightkg per unit
stackMax per stack (default 1)
usableCan be used from the inventory
consumeUnits used per use (default 1; 0 keeps the item)
durabilityTracks metadata.durability from 100 to 0
weapon{ ammo = 'ammo_pistol', magazine = 12 }, or {} for melee
ammoMarks an ammo item
container{ width, height, maxWeight } for bags
clothing{ slot, male = { drawable, texture }, female = { ... }, arms?, warmth }
deathDropDeath drop chance override, 0-1
lua
-- data/items.lua
scrap_radio = {
    label = 'Broken Radio',
    description = 'Might be worth something for parts.',
    category = 'material', size = { 2, 1 }, weight = 0.8, stack = 4,
    usable = true, consume = 1,
},

See Add an item for a full walkthrough.

Exports

Server: the inventory contract

These names are a contract that other resources rely on through encore.inventory.* in the en-core library. See Inventory interface.

ExportArgumentsReturns
AddItemsource, item, count, metadata?boolean. Tries the player grid, then the worn bag (bags never go into bags)
RemoveItemsource, item, count, metadata?boolean. All or nothing across grid and bag
CanCarryItemsource, item, count, metadata?boolean
GetItemCountsource, item, metadata?Number; metadata acts as a filter
GetItemDefinitionitemCopy of the definition with name, or nil
RegisterUsableItemitem, handlerhandler(source, itemName, { id, name, count, metadata }). The item is consumed only if the handler returns true

Server: additional

ExportArgumentsReturns
OpenInventorysource, requestOpens the inventory on the client. request = { kind = 'ground'|'drop'|'stash'|'trunk'|'glovebox', id?, netId?, class? }
GetInventoryidSnapshot of a loaded container, or nil
SetPersistentid, persistentboolean
RegisterStashid, databoolean. data = { label, width, height, maxWeight, coords, distance?, groups?, canOpen? }; coords is required. Defaults 6x4, 50 kg, 2.0 m
RemoveStashid
GetPlayerItemssource, name{ id, count, metadata }[]
SetItemMetadatasource, itemId, metadataboolean; merges fields
AddItemToInventoryid, item, count, metadata?boolean; any loaded container
ClearInventorysourceboolean
AddItemOrDropsource, item, count, metadata?placed, dropped. Anything that doesn't fit goes into a ground bag at the player's feet
GetEquippedWeaponsource{ id, name, metadata } or nil

Client

ExportArgumentsReturns
GetItemCountitem, metadata?Number (grid plus worn bag)
GetItemDefinitionitemDefinition or nil
OpenInventoryrequest?
CloseInventory
IsInventoryOpenboolean
GetCurrentWeapon{ item, name, ammo } or nil

Commands

Restricted to ServerConfig.adminPrincipal (default group.admin).

CommandWhat it does
/giveitem <target|me> <item> [count]Give an item to a player
/clearinventory <target>Remove every item from a player's inventory
/openinventory <target>Open another player's inventory beside your own
Note

en-admin also registers /giveitem with the same arguments. Both give the item through the inventory, so it works the same either way.

Events

Client (local)

Listen with AddEventHandler.

EventPayload
en-inventory:client:changedInventory snapshot
en-inventory:client:openedSecondary container snapshot
en-inventory:client:closed
en-inventory:client:weaponChangedweaponName|nil, itemId|nil
en-inventory:client:toggleWeaponitemId

Server (local)

EventPayload
en-inventory:server:playerChangedsrc. Fired when a worn bag's storage changes

en-inventory reacts to encore:server:onPlayerLoaded, encore:server:onPlayerUnload, encore:server:playerDied, en-clothing:server:loaded and txAdmin:events:serverShuttingDown on the server, and encore:client:playerLoaded, playerUnloaded and playerDied on the client.

Look options

With en-target running, en-inventory adds:

  • A global vehicle option en-inventory:trunk ("Open trunk") on the boot bone.
  • "Search bag" (or "Search" plus the bag label) on ground bag and death bag props.
  • A sphere zone "Open" plus the stash label for each config stash.

State bags

StateSet byUsed for
Player(src).state.carryBonusen-skillsExtra carry weight
Player(src).state.bagCapacityen-clothingExtra carry weight from a worn bag
Entity(vehicle).state.ownedVehicle resourcesTrunk and glovebox persistence

Database

TableColumns
encore_inventoriesid VARCHAR(120) primary key, type VARCHAR(20), items LONGTEXT (JSON), hotbar LONGTEXT, updated_at
encore_death_bagsid primary key, citizenid, name, coords (JSON), width, height, created_at INT

There is one row per container. A corrupt row refuses to load rather than being overwritten, so a bad write never wipes a player's items.

Examples

A usable bandage:

lua
-- server
encore.inventory.registerUsable('bandage', function(source, item, slot)
    local ped = GetPlayerPed(source)
    SetEntityHealth(ped, math.min(200, GetEntityHealth(ped) + 20))
    return true -- consume one
end)

A base storage box that another resource registers and opens:

lua
-- server
exports['en-inventory']:RegisterStash('en-bases:box:42', {
    label = 'Supply Crate', width = 8, height = 6, maxWeight = 120.0,
    coords = vec3(1200.0, -300.0, 69.0), distance = 2.0,
    canOpen = function(src) return true end,
})
lua
-- client
exports['en-inventory']:OpenInventory({ kind = 'stash', id = 'en-bases:box:42' })

Pay a reward that never gets lost when the player is full:

lua
-- server
local placed, dropped = exports['en-inventory']:AddItemOrDrop(source, 'cash', 250)
if dropped and dropped > 0 then
    encore.notify(source, { title = 'Inventory', description = 'Some of it fell at your feet.', type = 'inform' })
end