> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dietrich-development.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Developer reference

> DD Inventory server and client exports, events, item use handlers, move validators, and the secondary panel API

Everything a scripter needs to hook their own crafting, looting, banking or trading system into DD Inventory: the full server and client export surface, the events other resources can listen to, and the handler contracts.

<Info>
  Every export on this page is framework-neutral. The same call behaves identically on VORP, on RSG and on a standalone server, with two exceptions that are called out where they occur: currency reads and writes depend on `currencyMode`, and `setItemRarity` only writes back into a framework item table where one exists. For the optional compatibility surfaces (`vorp_inventory` and `rsg-inventory`), see the [Compatibility](/scripts/inventory/compatibility) page. For admin commands, see [Setup](/scripts/inventory/setup#admin-commands).
</Info>

## Conventions

Call server exports as `exports.dd_inventory:name(...)` from any server script. Unless noted, every mutating export returns a result table of the shape `{ ok = boolean, code = string? }` plus any export-specific fields. Read-only exports return the value directly, or `nil`.

Common error codes: `INVENTORY_NOT_FOUND`, `ITEM_NOT_FOUND`, `NO_CHARACTER`, `NO_LICENSE`, `EXCEEDS_MAX`, `BELOW_MIN`, `MOVE_BLOCKED`.

## Server exports

### Inventories

| Export                     | Signature                                      | Returns                                                            |
| -------------------------- | ---------------------------------------------- | ------------------------------------------------------------------ |
| `createInventory`          | `(invType, ownerCharId, maxSlots?, metadata?)` | new `inventory_id` (number)                                        |
| `getInventory`             | `(inventoryId)`                                | `{ data, state, items }` or `nil`                                  |
| `getPlayerInventory`       | `(charId)`                                     | the character's `personal` inventory, created on demand            |
| `getPlayerInventoryForNui` | `(charId, correlationId?)`                     | `{ inventory = <NUI payload>, definitions = <all defs> }`          |
| `evictInventoryCache`      | `(inventoryId)`                                | *nothing* - drops the server cache entry                           |
| `getCacheStats`            | `()`                                           | `{ cachedInventories, cachedItems, hits, misses, hitRate }`        |
| `sortInventory`            | `(inventoryId)`                                | `{ ok, code? }` - type-priority then alphabetical, compacts stacks |

`invType` is a free-form string. `personal`, `storage`, `vehicle`, `shop`, `guild`, `bank_guild`, `bank_accountwide`, `account_bank` and `mail_storage` all carry meaning elsewhere in the system - see `bindRestrictions` in [Setup](/scripts/inventory/setup#bind-types-and-bind-restrictions).

The returned inventory table is `{ data = <inventories row>, state = { [slot] = { <instances> } }, items = { <all instances> } }`.

```lua theme={null}
local invId = exports.dd_inventory:createInventory('storage', nil, 30, { label = 'Saloon cellar' })
local inv   = exports.dd_inventory:getInventory(invId)
print(inv.data.max_slots, #inv.items)
```

### Items

| Export                        | Signature                                                               | Returns                                                                                         |
| ----------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `addItem`                     | `(inventoryId, itemName, quantity, metadata?, reason?, correlationId?)` | `{ ok, code?, itemIds? }`                                                                       |
| `addItemToCharacterInventory` | `(charId, itemName, quantity, metadata?, reason?, correlationId?)`      | same as `addItem`                                                                               |
| `simulateAddItems`            | `(charId, items)`                                                       | `{ ok, code?, slotsConsumed? }` - dry run, mutates nothing                                      |
| `removeItem`                  | `(inventoryId, itemName, quantity, metadata?)`                          | `{ ok, code? }`                                                                                 |
| `removeItemById`              | `(itemId)`                                                              | `{ ok, code? }`                                                                                 |
| `confirmConsume`              | `(source, itemId)`                                                      | `{ ok, code? }` - validates ownership and usability, removes, fires `dd_inventory:itemConsumed` |
| `getItemById`                 | `(itemId)`                                                              | the `item_instances` row or `nil`                                                               |
| `getSlotContents`             | `(inventoryId, slot)`                                                   | array of instances in that slot                                                                 |
| `hasItem`                     | `(inventoryId, itemName, quantity?)`                                    | boolean                                                                                         |
| `moveItems`                   | `(fromInvId, toInvId, itemIds, toSlot?)`                                | `{ ok, code? }`                                                                                 |
| `swapSlots`                   | `(inventoryId, slotA, slotB)`                                           | `{ ok, code? }`                                                                                 |
| `setItemMetadata`             | `(itemId, metadata)`                                                    | `{ ok, code? }`                                                                                 |
| `setItemBound`                | `(itemId)`                                                              | `{ ok, code? }` - marks a `bind_on_equip` instance as bound                                     |

`simulateAddItems` takes `{ { name = 'apple', quantity = 3, metadata = {...} }, ... }` and is the correct pre-flight for any reward or purchase flow that must not partially succeed.

```lua theme={null}
local check = exports.dd_inventory:simulateAddItems(charId, {
    { name = 'apple', quantity = 3 },
    { name = 'gold_bar', quantity = 1 },
})
if not check.ok then return end -- e.g. code = 'NOT_ENOUGH_SLOTS'
exports.dd_inventory:addItemToCharacterInventory(charId, 'apple', 3, nil, 'quest_reward')
```

### Item definitions and rarity

| Export                      | Signature               | Returns                                                                                                        |
| --------------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------- |
| `getItemDefinition`         | `(itemName)`            | the cached `item_definitions` row or `nil`                                                                     |
| `registerItemDefinition`    | `(defData)`             | `{ ok, code? }` - upserts a definition                                                                         |
| `adminReloadItemDefinition` | `(itemName)`            | `{ ok, code?, definition?, clientsPushed? }` - reload one definition from the database and push to all clients |
| `getItemRarity`             | `(itemName)`            | rarity key (string) or `nil` if the item is unknown                                                            |
| `setItemRarity`             | `(itemName, rarityKey)` | `{ ok, code?, warning?, previous?, rarity?, frameworkSynced?, clientsPushed? }`                                |
| `getItemIcon`               | `(itemName)`            | icon path or `nil`                                                                                             |
| `getItemIconMap`            | `()`                    | `{ [itemName] = iconPath }` for every scanned icon                                                             |
| `getCurrencyConfig`         | `()`                    | the same `currencyConfig` payload the NUI receives                                                             |

`registerItemDefinition` fields: `item_name` *(required)*, `label` *(required)*, `description`, `stack_size` (default `1`), `item_type` (default `misc`), `rarity` (default `common`), `weight` (default `0.0`), `sell_value`, `is_usable`, `metadata_schema`, `default_metadata`, `bind_type` (`tradable` | `bind_on_equip` | `soulbound` | `family_bound`), `equipment_data`.

`setItemRarity` always writes `item_definitions.rarity`. On VORP it additionally writes the `rarity` column DD Inventory injects into the `items` table (`frameworkSynced = true`), which is what makes the change survive the boot-time re-import. On RSG and standalone there is no such column, so `frameworkSynced` comes back false and the definition write is the whole story. It then reloads the definition cache and pushes fresh state only to online players actually holding the item (`clientsPushed`). Unknown keys are accepted and reported back as `warning = 'UNKNOWN_RARITY_KEY'` with `ok = true`.

```lua theme={null}
exports.dd_inventory:registerItemDefinition({
    item_name  = 'relic_compass',
    label      = 'Tarnished Compass',
    item_type  = 'quest',
    rarity     = 'epic',
    weight     = 0.4,
    stack_size = 1,
    bind_type  = 'soulbound',
})

local r = exports.dd_inventory:setItemRarity('relic_compass', 'legendary')
-- { ok = true, previous = 'epic', rarity = 'legendary', frameworkSynced = true, clientsPushed = 2 }

print(exports.dd_inventory:getItemRarity('relic_compass')) -- legendary
```

### Currency and account sigils

Two character wallets and one account-wide premium balance. The keys are internal identifiers, never shown to a player - players only ever see the `label` you set in `DDInvConfig.currencies`.

| Key          | `currencyMode = 'framework'`                                                                                                       | `currencyMode = 'native'`                 |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| `breakdust`  | the cash wallet: VORP currency `0`, or rsg-core's `cash` account                                                                   | DD Inventory's `character_currency` table |
| `seamshards` | the gold wallet: VORP currency `1`, or rsg-core's `gold` account. RSG's `bloodmoney` is a separate currency and is never used here | DD Inventory's `character_currency` table |
| `sigils`     | the account-wide premium balance, DD Inventory-owned in both modes and keyed on the account license                                | same                                      |

<Warning>
  In `'framework'` mode these exports read and write the host framework's wallet through `dd_lib`. On a **standalone** server there is no wallet to reach, so `getCharacterCurrency` returns zeros and `addCurrency` / `removeCurrency` fail. Standalone deployments must run `currencyMode = 'native'`, see [Setup](/scripts/inventory/setup#currency-mode). `sigils` is unaffected on every framework.
</Warning>

In `'framework'` mode an offline character has no reachable wallet either, because the framework wallet is only addressable through a connected player object. Reads report zero and writes return `PLAYER_OFFLINE`. In `'native'` mode offline characters work normally.

| Export                 | Signature                                      | Returns                                                      |
| ---------------------- | ---------------------------------------------- | ------------------------------------------------------------ |
| `getCharacterCurrency` | `(charId)`                                     | `{ breakdust, seamshards, sigils }`                          |
| `addCurrency`          | `(charId, currencyType, amount)`               | `{ ok, code? }` - pushes the new balance to the client       |
| `removeCurrency`       | `(charId, currencyType, amount)`               | `{ ok, code? }`                                              |
| `transferCurrency`     | `(fromCharId, toCharId, currencyType, amount)` | `{ ok, code? }` - `sigils` returns `SIGILS_NOT_TRANSFERABLE` |
| `getAccountSigils`     | `(license)`                                    | number                                                       |
| `addAccountSigils`     | `(license, amount)`                            | `{ ok, code? }`                                              |
| `removeAccountSigils`  | `(license, amount)`                            | `{ ok, code? }`                                              |

Passing `'sigils'` to `addCurrency` or `removeCurrency` transparently routes to the account-wide balance for that character's license.

```lua theme={null}
exports.dd_inventory:addCurrency(charId, 'breakdust', 25) -- 25 cash
exports.dd_inventory:addAccountSigils(license, 100)
```

### Weight

| Export                  | Signature          | Returns                                             |
| ----------------------- | ------------------ | --------------------------------------------------- |
| `getCharacterWeight`    | `(charId)`         | current carried weight (number)                     |
| `getCharacterMaxWeight` | `(charId)`         | effective max weight (number)                       |
| `getOverweightRatio`    | `(charId)`         | `weight / maxWeight` (number)                       |
| `isCharacterOverweight` | `(charId)`         | boolean                                             |
| `setMaxWeight`          | `(charId, newMax)` | `{ ok, code? }` - persists and pushes               |
| `recheckWeightDebuff`   | `(charId)`         | *nothing* - re-evaluates the overweight tier debuff |

### Slots and slot bonuses

| Export                 | Signature                       | Returns                                                              |
| ---------------------- | ------------------------------- | -------------------------------------------------------------------- |
| `upgradeMaxSlots`      | `(inventoryId, newMax)`         | `{ ok, code? }` - rejects `newMax <= current`                        |
| `addMaxSlots`          | `(charId, additionalSlots)`     | `{ ok, code? }` - capped at `DDInvConfig.maxMaxSlots`                |
| `decrementMaxSlots`    | `(charId, slotsToRemove)`       | `{ ok, code?, newMax? }` - never below `DDInvConfig.defaultMaxSlots` |
| `getEffectiveMaxSlots` | `(inventoryData, charId)`       | base slots plus stat/buff bonus                                      |
| `getSlotBonus`         | `(base, charId, inventoryType)` | bonus slot count                                                     |
| `getSlotStatKey`       | `(inventoryType)`               | the stat key that grants bonus slots for that type                   |

### Use handlers and move validators

| Export                          | Signature                                   | Returns   |
| ------------------------------- | ------------------------------------------- | --------- |
| `registerItemUseHandler`        | `(itemName, handler, opts?)`                | boolean   |
| `unregisterItemUseHandler`      | `(itemName)`                                | *nothing* |
| `hasItemUseHandler`             | `(itemName)`                                | boolean   |
| `registerMoveValidator`         | `(key, validatorFn)` *(or `(validatorFn)`)* | *nothing* |
| `registerSecondaryCloseHandler` | `(handler)`                                 | *nothing* |

#### Use handlers

Lua function references cannot be marshalled across resource VM boundaries, so a handler registered from another resource must be given as a `(resourceName, exportName)` pair. Three accepted shapes:

```lua theme={null}
-- from another resource - the only reliable cross-VM form
exports.dd_inventory:registerItemUseHandler('value_pack_7d', 'dd_consumables', 'handleValuePack7d')
exports.dd_inventory:registerItemUseHandler('value_pack_7d', {
    resource = 'dd_consumables', exportName = 'handleValuePack7d',
})
-- in-process only (inside dd_inventory itself)
exports.dd_inventory:registerItemUseHandler('apple', function(ctx) ... end)
```

Handler contract:

```lua theme={null}
-- ctx    = { charId, source, item, def, correlationId }
-- return = { ok = boolean, code? = string, message? = string,
--            consume? = boolean, rollback? = function }
function HandleValuePack(ctx)
    exports.dd_inventory:addMaxSlots(ctx.charId, 4)
    return { ok = true, consume = true }
end
```

Registration is last-writer-wins, so hot-reloading the owning resource is safe. `opts.cooldownMs` is accepted for forward compatibility but is not yet enforced.

<Note>
  Use handling runs with in-flight locking, automatic consume and rollback on failure, so an item that grants something can never be duplicated.
</Note>

#### Move validators

Move validators run on every player-initiated cross-inventory move and can veto it:

```lua theme={null}
exports.dd_inventory:registerMoveValidator('dd_bank', function(source, fromInvId, toInvId, itemIds)
    if not playerHasBankAccess(source, toInvId) then
        return { allowed = false, code = 'NO_BANK_ACCESS', message = 'You have no access to that vault.' }
    end
    return { allowed = true }
end)
```

Passing a key, as above, replaces any validator previously registered under that key, which keeps hot-reload clean. `message`, when present, is shown to the player as an error notification.

#### Secondary close handlers

Called when a player closes the secondary panel, so the owning resource (bank, shop, stash) can release its lock:

```lua theme={null}
exports.dd_inventory:registerSecondaryCloseHandler(function(source, inventoryId)
    releaseVaultLock(inventoryId)
end)
```

### Per-inventory access control

Any inventory can carry an access control list in the `inventory_rights` table. It answers three questions per character: who may open it, who may take out of it and who may put into it.

<Warning>
  **An inventory with no entries is open to everyone.** That is the default and it never changes on its own, so this feature costs an existing server nothing until you grant the first right. Granting it turns that one inventory into a whitelist; revoking the last one opens it again. The owner of an inventory (`inventories.owner_char_id`) always holds full rights and cannot be locked out.
</Warning>

| Export                  | Signature                                       | Returns                                           |
| ----------------------- | ----------------------------------------------- | ------------------------------------------------- |
| `grantInventoryRight`   | `(inventoryId, charId, permission, grantedBy?)` | `{ ok, code? }`                                   |
| `revokeInventoryRight`  | `(inventoryId, charId, permission?)`            | `{ ok, code? }` - omit `permission` to revoke all |
| `setInventoryRights`    | `(inventoryId, entries, grantedBy?)`            | `{ ok, code? }` - replaces the whole list         |
| `clearInventoryRights`  | `(inventoryId)`                                 | `{ ok }`                                          |
| `getInventoryRights`    | `(inventoryId)`                                 | `{ { charId, view, take, put }, ... }`            |
| `isInventoryRestricted` | `(inventoryId)`                                 | boolean                                           |
| `hasInventoryRight`     | `(inventoryId, charId, action)`                 | boolean - `action` is `view`, `take` or `put`     |
| `reloadInventoryRights` | `()`                                            | number of rows loaded                             |

| Permission | Grants                                     |
| ---------- | ------------------------------------------ |
| `view`     | Open the inventory and see what is inside. |
| `take`     | Move items out of it. Implies `view`.      |
| `put`      | Move items into it. Implies `view`.        |
| `full`     | All three.                                 |

The common synonyms resolve too (`open`, `withdraw`, `deposit`, `admin`, …), so hand-written rows do something sensible.

```lua theme={null}
-- Only character 42 may open this stash, and only to take things out of it
exports.dd_inventory:grantInventoryRight(vaultId, 42, 'take')

-- Replace the whole list in one call (an empty list reopens the inventory)
exports.dd_inventory:setInventoryRights(vaultId, {
    { charId = 42, permission = 'full' },
    { charId = 77, permission = 'put'  },
})

exports.dd_inventory:revokeInventoryRight(vaultId, 77)      -- one character
exports.dd_inventory:clearInventoryRights(vaultId)          -- everyone
```

Enforcement happens in two places, both server-side:

* **Opening** - the secondary-open callback and `openInventoryForPlayer` (which every VORP and RSG compatibility open path funnels through) check `view`.
* **Moving** - a built-in validator registered under the key `dd_inventory_rights` in the same registry as `registerMoveValidator`, so the access list, DD Bank's guild checks and the VORP container rules queue up together instead of contradicting each other. Cross-inventory moves need `take` on the source and `put` on the target; rearranging inside one inventory needs `view`.

The table is read once at start and kept in memory, so a move never costs a database query. After editing `inventory_rights` with raw SQL, call `reloadInventoryRights()`.

VORP custom inventories registered with `UsePermissions` have their `CharIdPermissionMoveTo` and `CharIdPermissionTakeFrom` entries projected onto this table on registration, which is what makes those permissions enforced rather than merely stored.

### Secondary panel

Banks, stashes, shops and storages render inside the inventory as a second panel.

| Export                    | Signature               | Returns                                                               |
| ------------------------- | ----------------------- | --------------------------------------------------------------------- |
| `openInventoryForPlayer`  | `(source, inventoryId)` | `{ ok, code? }` - opens the inventory as the player's secondary panel |
| `closeInventoryForPlayer` | `(source, inventoryId)` | `{ ok = true }`                                                       |

```lua theme={null}
local vaultId = exports.dd_inventory:createInventory('storage', nil, 40)
exports.dd_inventory:openInventoryForPlayer(source, vaultId)
```

### Player push, open and misc

| Export                  | Signature                          | Returns                                                                                                                                      |
| ----------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `pushInventoryToPlayer` | `(charId)`                         | *nothing* - re-sends the character's inventory to their client                                                                               |
| `requestTrade`          | `(sourceServerId, targetServerId)` | `{ ok, code? }` - opens a player-to-player trade request. `code = 'TOO_FAR'` when the two players are further apart than `trade.maxDistance` |
| `getActiveTrades`       | `()`                               | list of trades in flight, each with the live distance between the players and the limit it is measured against                               |

Call `pushInventoryToPlayer` after any out-of-band database write you performed yourself, so the open NUI does not show stale contents.

## Client exports

Call as `exports.dd_inventory:name(...)` from any client script.

| Export                     | Signature                 | Returns                                                      |
| -------------------------- | ------------------------- | ------------------------------------------------------------ |
| `openInventory`            | `()`                      | *nothing*                                                    |
| `closeInventory`           | `()`                      | *nothing*                                                    |
| `toggleInventory`          | `()`                      | *nothing*                                                    |
| `isInventoryOpen`          | `()`                      | boolean                                                      |
| `openSecondaryInventory`   | `(inventoryId, options?)` | *nothing*                                                    |
| `switchSecondaryInventory` | `(inventoryId, options?)` | *nothing* - swap the panel without closing it                |
| `closeSecondaryInventory`  | `()`                      | *nothing*                                                    |
| `getSecondaryInventoryId`  | `()`                      | current secondary inventory id or `nil`                      |
| `updateSecondaryContext`   | `(contextData)`           | *nothing* - refresh the secondary panel's header and context |
| `updatePrimaryCurrency`    | `(currency)`              | *nothing* - refresh the currency bar                         |
| `setMailAttachMode`        | `(active, maxSlots)`      | *nothing* - put the panel into attachment-picking mode       |
| `getMailAttachItemIds`     | `()`                      | array of selected `item_id`s                                 |
| `requestTrade`             | `(targetServerId)`        | *nothing* - start a trade with another player                |

Three more come from the client-side vorp compatibility layer (`client/compat/vorp_client.lua`). That file is not gated on the detected framework, so these exist on every framework, but only a VORP server is likely to have callers for them:

| Export              | Signature | Notes                                                                  |
| ------------------- | --------- | ---------------------------------------------------------------------- |
| `isBlocked`         | `()`      | whether inventory opening is currently blocked                         |
| `getInventoryItems` | `()`      | **stub** - returns `{}` and prints a warning; query the server instead |
| `getInventoryItem`  | `(name)`  | **stub** - returns `nil`; query the server instead                     |

```lua theme={null}
if not exports.dd_inventory:isInventoryOpen() then
    exports.dd_inventory:openInventory()
end
```

## Events

### Server-side broadcasts

These fire in DD Inventory's server VM and are visible to every resource. Listen with `AddEventHandler`.

| Event                       | Args                                  | When                                                                                              |
| --------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `dd_inventory:created`      | `(inventoryId, invType, ownerCharId)` | a new inventory row is created                                                                    |
| `dd_inventory:updated`      | `(inventoryId, action, detail)`       | any content change - `action` is `add`, `remove`, `move`, `swap`, `transfer_in` or `transfer_out` |
| `dd_inventory:transferred`  | `(fromInvId, toInvId, movedItemIds)`  | items crossed an inventory boundary                                                               |
| `dd_inventory:itemConsumed` | `(source, itemName, itemType)`        | `confirmConsume` succeeded - the hook for achievement and statistics resources                    |

The event names are also exposed as the constants `DDEvents.INVENTORY_CREATED`, `DDEvents.INVENTORY_UPDATED` and `DDEvents.INVENTORY_TRANSFERRED` in `shared/dd_compat.lua`. Prefer the constants over the literal strings.

```lua theme={null}
AddEventHandler('dd_inventory:updated', function(inventoryId, action, detail)
    if action == 'add' then
        print(('inventory %d gained %dx %s'):format(inventoryId, detail.quantity, detail.itemName))
    end
end)

AddEventHandler('dd_inventory:itemConsumed', function(source, itemName, itemType)
    Achievements.Bump(source, 'items_consumed', 1)
end)
```

### Client-side broadcasts

| Event                                                       | Args                           | When                                                                                                                                                      |
| ----------------------------------------------------------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dd_inventory:itemUsed`                                     | `(itemName, metadata, itemId)` | the player right-clicked an item and the server allowed the use with **no** server-side handler registered - the legacy hook for client-side item effects |
| `dd_inventory:client:opened` (`DDEvents.INV_CLIENT_OPENED`) | *none*                         | the inventory NUI opened                                                                                                                                  |
| `dd_inventory:client:closed` (`DDEvents.INV_CLIENT_CLOSED`) | *none*                         | the inventory NUI closed                                                                                                                                  |

```lua theme={null}
AddEventHandler('dd_inventory:itemUsed', function(itemName, metadata, itemId)
    if itemName == 'pocket_watch' then ShowTimeHud() end
end)
```

### Internal net events

<Warning>
  `dd_inventory:client:statePush`, `dd_inventory:sync:push` (`DDEvents.INV_SYNC_CLIENT`), `dd_inventory:open`, `dd_inventory:move`, `dd_inventory:use`, `dd_inventory:drop`, `dd_inventory:slots:purchase` and `dd_inventory:server:viewSecondary` are the NUI's own transport between client and server. They are listed here so you recognise them in logs - do not trigger them from other resources, use the exports above instead.
</Warning>

## Audit log

Every add, remove, move, swap, trade and use is written to `inventory_audit` with a correlation id, so a single player action is traceable end to end. Pass your own `correlationId` into `addItem` or `addItemToCharacterInventory` to tie inventory writes to your resource's own transaction, then read them back with `/dd_debug audit <correlationId>`.
