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 |
| Optional | en-clothing (worn slots and bags), en-target (trunk, bag and stash options), en-skills (carry bonus) |
| Start after | oxmysql, en-core, en-ui. Resources that register usable items re-register automatically when the inventory starts |
| Database tables | encore_inventories, encore_death_bags (created automatically) |
| Config files | config/shared.lua (sent to clients), config/server.lua (server only), data/items.lua |
| Key binds | en_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 }.xandyare the top-left cell, starting at 1. A stack is one instance withcountabove 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.
AddItemandCanCarryItemuse the same plan, so they always agree. - Weight is
weight x count, plusmetadata.contentWeightfor 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
| Container | Id | Notes |
|---|---|---|
| Stash | stash:<id> | Fixed world stashes from config, or registered by other resources |
| Trunk | trunk:<plate> | Size by vehicle class; persists only when Entity(vehicle).state.owned == true |
| Glovebox | glovebox:<plate> | You must be in the vehicle; same persistence rule as trunks |
| Ground bag | bag:<random> | Client-local props within 60 m; despawn after 30 minutes untouched |
| Death bag | death:<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):
- A share of carried cash (
cashPercent) always drops. - Each stack rolls to drop: the item's
deathDrop, else its category chance, elseitemChance. - Each worn clothing piece rolls
clothingChance. A worn bag rollsbagChanceand 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
| Key | Default | What it does |
|---|---|---|
Config.player.width / height | 8 / 6 | Player grid size in cells |
Config.player.maxWeight | 30.0 | Base carry weight (kg) |
Config.player.hotbarSlots | 5 | Hotbar slots (keys 1-5) |
Config.drop | 8x6, maxWeight 500.0, prop 'prop_cs_heist_bag_02', range 2.5, renderDistance 60.0 | Ground 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.trunkDistance | 3.5 | How close to the trunk you must stand |
Config.vehicles.trunk[class] | see below | Trunk 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.disableWheel | true | Block the GTA weapon wheel and weapon number keys |
Config.weapons.ammoSyncInterval | 5000 ms | How often the client reports rounds fired |
Config.weapons.wearPerShot | 0.05 (not in the file by default) | Durability lost per shot |
Config.images | true | Use 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
| Key | Default | What it does |
|---|---|---|
ServerConfig.autosaveInterval | 60000 ms | Changed inventories are flushed to the database |
ServerConfig.dropLifetime | 30 * 60 s | Untouched ground bags despawn |
ServerConfig.death.enabled | true | Death bags on or off |
ServerConfig.death.cashPercent | 0.5 | Share of carried cash that always drops |
ServerConfig.death.itemChance | 0.4 | Drop chance per stack |
ServerConfig.death.categories | { weapon = 0.6, ammo = 0.5, key = 0.0 } | Per-category overrides |
ServerConfig.death.clothingChance | 0.25 | Drop chance per worn clothing piece |
ServerConfig.death.bagChance | 0.5 | Drop chance for a worn bag (with its contents) |
ServerConfig.death.lifetimeHours | 48 | Death 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.stashes | one example, burton_locker | Fixed world stashes |
A stash entry takes id, label, coords, distance, width, height, maxWeight, plus optional personal, shared and groups. groups accepts:
| Value | Who 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 |
-- 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.
| Field | What it does |
|---|---|
label, description | Display text |
category | food, drink, medical, weapon, ammo, tool, material, clothing, equipment, misc, key or currency |
size | { w, h } footprint in cells |
weight | kg per unit |
stack | Max per stack (default 1) |
usable | Can be used from the inventory |
consume | Units used per use (default 1; 0 keeps the item) |
durability | Tracks metadata.durability from 100 to 0 |
weapon | { ammo = 'ammo_pistol', magazine = 12 }, or {} for melee |
ammo | Marks an ammo item |
container | { width, height, maxWeight } for bags |
clothing | { slot, male = { drawable, texture }, female = { ... }, arms?, warmth } |
deathDrop | Death drop chance override, 0-1 |
-- 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.
| Export | Arguments | Returns |
|---|---|---|
AddItem | source, item, count, metadata? | boolean. Tries the player grid, then the worn bag (bags never go into bags) |
RemoveItem | source, item, count, metadata? | boolean. All or nothing across grid and bag |
CanCarryItem | source, item, count, metadata? | boolean |
GetItemCount | source, item, metadata? | Number; metadata acts as a filter |
GetItemDefinition | item | Copy of the definition with name, or nil |
RegisterUsableItem | item, handler | handler(source, itemName, { id, name, count, metadata }). The item is consumed only if the handler returns true |
Server: additional
| Export | Arguments | Returns |
|---|---|---|
OpenInventory | source, request | Opens the inventory on the client. request = { kind = 'ground'|'drop'|'stash'|'trunk'|'glovebox', id?, netId?, class? } |
GetInventory | id | Snapshot of a loaded container, or nil |
SetPersistent | id, persistent | boolean |
RegisterStash | id, data | boolean. data = { label, width, height, maxWeight, coords, distance?, groups?, canOpen? }; coords is required. Defaults 6x4, 50 kg, 2.0 m |
RemoveStash | id | |
GetPlayerItems | source, name | { id, count, metadata }[] |
SetItemMetadata | source, itemId, metadata | boolean; merges fields |
AddItemToInventory | id, item, count, metadata? | boolean; any loaded container |
ClearInventory | source | boolean |
AddItemOrDrop | source, item, count, metadata? | placed, dropped. Anything that doesn't fit goes into a ground bag at the player's feet |
GetEquippedWeapon | source | { id, name, metadata } or nil |
Client
| Export | Arguments | Returns |
|---|---|---|
GetItemCount | item, metadata? | Number (grid plus worn bag) |
GetItemDefinition | item | Definition or nil |
OpenInventory | request? | |
CloseInventory | ||
IsInventoryOpen | boolean | |
GetCurrentWeapon | { item, name, ammo } or nil |
Commands
Restricted to ServerConfig.adminPrincipal (default group.admin).
| Command | What 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 |
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.
| Event | Payload |
|---|---|
en-inventory:client:changed | Inventory snapshot |
en-inventory:client:opened | Secondary container snapshot |
en-inventory:client:closed | |
en-inventory:client:weaponChanged | weaponName|nil, itemId|nil |
en-inventory:client:toggleWeapon | itemId |
Server (local)
| Event | Payload |
|---|---|
en-inventory:server:playerChanged | src. 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 thebootbone. - "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
| State | Set by | Used for |
|---|---|---|
Player(src).state.carryBonus | en-skills | Extra carry weight |
Player(src).state.bagCapacity | en-clothing | Extra carry weight from a worn bag |
Entity(vehicle).state.owned | Vehicle resources | Trunk and glovebox persistence |
Database
| Table | Columns |
|---|---|
encore_inventories | id VARCHAR(120) primary key, type VARCHAR(20), items LONGTEXT (JSON), hotbar LONGTEXT, updated_at |
encore_death_bags | id 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:
-- 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:
-- 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,
})-- client
exports['en-inventory']:OpenInventory({ kind = 'stash', id = 'en-bases:box:42' })Pay a reward that never gets lost when the player is full:
-- 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