# Developer > Exports, hooks and state bags for ML Inventory Category: INVENTORY · Source: https://miciomods.it/docs/ml-inventory-developer · Last updated: 2026-10-02 # Developer The resource folder is named `ml_inventory` and answers to the name `ox_inventory`, so both `exports.ox_inventory` and `exports.ml_inventory` reach the same functions. ML Clothing carries its own server and client exports plus `open/server.lua` and `open/adapters/`, the hooks and adapters a server owner can edit. ## The ox_inventory API is unchanged Every export, event and hook of the ox_inventory install keeps its name and its arguments. Scripts calling `exports.ox_inventory` need no edit. ```lua local count = exports.ox_inventory:GetItemCount(source, 'water') ``` Hooks registered with `exports.ox_inventory:registerHook` keep firing, `swapItems` included: a move inside the grid fires it with the same payload as an upstream install. ## Hook bus Any resource can register a handler. Handlers registered by a resource are dropped when that resource stops. ```lua exports.ox_inventory:mlRegisterHook('beforeItemDelete', function(ctx) if ctx.itemName == 'evidence_bag' then return 'Evidence cannot be destroyed' end end) ``` `mlRegisterHook(name, handler)` takes a function, or a table `{ fn = handler, priority = 50 }`. Lower priority runs first, the default is 50. The call returns an id, or nothing when the name is not a string or the handler is not callable. `mlUnregisterHook(name, id)` removes one handler and returns `true` when it found it. Client and server handlers are separate: a handler registered on the client only sees client hook points, a handler registered on the server only sees server ones. A handler that errors is logged, and the run continues with the next handler. ### How a hook runs - `mlRunHook(name, context)`: every handler runs in priority order. A handler returning `false` stops the run and the action. A handler returning a string stops it and the string comes back as the reason. Returns `ok, reason?`. - `mlEmitHook(name, context)`: every handler runs, return values are ignored, nothing is returned. - `mlTransformHook(name, context)`: a handler returning a table replaces the context for the handlers after it and for the caller. A handler returning `false` cancels. No shipped hook point runs in transform mode. It is there for hook points another resource emits. ### Hook points - `beforeItemDelete`, server, run mode. Fields: `source`, `inventoryId`, `slot`, `itemName`, `count`, `metadata`. Fires when a player destroys an item from the interface. Returning `false` or a string refuses the deletion. - `afterItemDelete`, server, emit mode. Fields: `source`, `inventoryId`, `slot`, `itemName`. Fires after the item is gone. - `afterInventoryOpen`, client, emit mode. Field: `inventory`. Fires when the interface reports itself open. - `afterInventoryClose`, client, emit mode, no fields. ## Turning features off for one player ```lua exports.ox_inventory:mlDisableFeature(source, 'give', true) ``` Server export. `mlDisableFeature(source, feature, disabled)` returns `true` when the flag was set, `false` when the feature name is unknown or the player is not connected. The feature keys: - `grid`: every move, split and stack is refused and the sort button does nothing. - `clothing`: moves into or out of the clothing slots are refused, the rest of the grid still works. - `loot`: `OpenLootStash` returns nothing for that player. - `give`: giving an item is refused and the player is notified. - `admin`: the admin panel refuses to open for that player, whatever permission they hold. `mlIsFeatureDisabled(source, feature)` returns the current flag as a boolean. `mlDisableAllFeatures(source, disabled)` covers every key above at once and returns nothing. Every flag is dropped when the player leaves. ## Columns of a single inventory ```lua exports.ox_inventory:mlSetInventoryCols('gang_stash_1', 5) ``` Server export. `mlSetInventoryCols(invId, cols)` sets the grid width of one inventory by id and returns nothing. Only a whole number of 1 or more sets a width. A fraction, a value below 1 or anything that is not a number clears the override. Without an override the width comes from the grid config. **micio/config/grid.lua** ```lua playerCols = 8, dropCols = 6, secondaryCols = 6, ``` - `playerCols`: grid width of the player inventory when no override is set. - `dropCols`: grid width of a ground drop. - `secondaryCols`: grid width of any other inventory that has no width of its own. An inventory that carries its own `cols` uses that, and a stash or container takes a width from its slot count. ## Registering a stash with a width `RegisterStash` takes the width in two ways. Pass the slots as a table: ```lua exports.ox_inventory:RegisterStash('gang_stash_1', 'Gang Stash', { slots = 30, cols = 5 }, 100000) ``` Or, when the call already passes an instance as the ninth argument, the eighth is the width: ```lua exports.ox_inventory:RegisterStash('gang_stash_1', 'Gang Stash', 30, 100000, nil, nil, nil, 5, instanceId) ``` A call with eight arguments keeps the stock meaning, where the eighth argument is the instance. A number between 1 and 24 in that position is passed through as the instance and the server prints a line pointing at `mlSetInventoryCols`. `CreateTemporaryStash` reads `cols` from its properties table and applies it to the stash it just created. ## Loot stashes ```lua local id = exports.ox_inventory:OpenLootStash(source, 'stash', { id = 'crate_01' }, { persistent = true }) ``` Server export. `OpenLootStash(playerId, invType, data, lootConfig?)` opens an inventory for one player as a loot container: what is inside stays hidden until the player uncovers it. The first three arguments are the ones `forceOpenInventory` takes. The fourth is handed to the interface. Only `persistent` is read by the server, and on a stored inventory it is required. On a temporary inventory it is not needed. Returns the inventory id, or nothing when the inventory does not exist, when the `loot` feature is disabled for that player, or when a loot config without `persistent` is aimed at a stored inventory. ## Sounds ```lua exports.ox_inventory:mlPlayInventorySound('reveal_rare') ``` Client export. `mlPlayInventorySound(event)` plays one of the interface sounds and returns `true`, or `false` when the event name is unknown. The event names are `container_open`, `scan_tick`, `slot_unlock`, `reveal_common`, `reveal_uncommon`, `reveal_rare`, `reveal_epic`, `reveal_legendary` and `loot_all`. ML Clothing exposes the same list through `exports.ml_clothing:PlayLootSound(data)`, where `data` is an event name or a table with an `event` field. It returns nothing and does nothing while ML Inventory is not started. ## Other public exports ```lua local refreshed = exports.ox_inventory:RefreshItemDefinition('water') ``` Server export. `RefreshItemDefinition(itemName)` re-reads the item definition and pushes label, category, rarity, description, stack, `maxstock`, `close`, size and weight into every slot that holds that item, on every loaded inventory. Returns the number of inventories it touched, `0` when the item name is unknown. The admin panel calls it after an item is edited, so an item change reaches players without a restart. ```lua local item = exports.ox_inventory:mlHeldItem() ``` Client export. `mlHeldItem()` returns the name of the item the player is currently holding in hand, or `false` when nothing is held. ## ML Clothing server exports ```lua exports.ml_clothing:GetReservedSlots() ``` Returns the sorted list of clothing slot numbers, 11 to 25. Use it to keep another script from writing into a reserved slot. ```lua exports.ml_clothing:GetClothingSlotMap() ``` Returns `{ slots, plates, stashPrefixes }`: the slot allowed for each clothing item, the plate definitions, and the two stash prefixes for plate carriers and bags. This is the table ML Inventory reads at boot to learn the layout. ```lua exports.ml_clothing:RegisterEquipmentStash(stashType, uniqueId, slots, maxWeight, cols?) ``` `stashType` is `'plateCarrier'` or `'bag'`. The id is the matching prefix from `Config.Stash` followed by `uniqueId`. The stash is registered through ML Inventory, so `cols` sets its grid width. Returns the full stash id. ```lua exports.ml_clothing:GetEquipmentStashId(stashType, uniqueId) ``` Returns the id `RegisterEquipmentStash` would build for the same arguments, without registering anything. ```lua exports.ml_clothing:GetWornDurability(src, slot) ``` Returns the `durability` value from the metadata of that slot, or `nil` when the slot is empty or carries no metadata. ```lua exports.ml_clothing:IsGearBroken(src, slot) ``` Returns `true` when the piece worn in that slot is currently flagged broken, otherwise `false`. ```lua exports.ml_clothing:RepairGear(src, slot, amount?) ``` Repairs the piece worn in that slot. `amount` is clamped between 1 and 100 and defaults to 100. Returns `false` when the arguments are not numbers, when the slot is not one of the slots wear is configured for, or when the repair itself fails; otherwise `true`. ## ML Clothing client exports ```lua exports.ml_clothing:getCurrentAppearance() ``` Returns the current ped appearance, components and props. ```lua exports.ml_clothing:applyAppearance(appearanceData) ``` Applies an appearance table to the local ped and returns nothing. ```lua exports.ml_clothing:syncFromNUI(appearanceData) ``` Sends an appearance table to the server as a new outfit to turn into clothing items, and returns nothing. It does nothing when `appearanceData` is missing. This is the call to add inside the appearance script wherever an outfit is saved. ```lua exports.ml_clothing:syncFromPed() ``` Reads the live ped components and props and sends them the same way. Use it when the appearance data is not at hand or its format has no adapter. ```lua exports.ml_clothing:clearSkin() ``` Resets the ped clothing components to their default state. Returns nothing. ```lua exports.ml_clothing:useVest(item, data) ``` Equips or removes the plate carrier, where `data` carries the `slot` and the item `metadata`. It registers the carrier when the metadata has no `carrierId` yet, plays the tie animation and sets ped armor from the health of the inserted plates. ```lua exports.ml_clothing:openGiveMode(data) ``` Closes the inventory and starts hand item to player targeting. The player aims at someone nearby and confirms to give the item. Returns nothing. ```lua exports.ml_clothing:lootTrapEffect() ``` Plays the trap reaction on the local ped: animation, screen effect, sound and 25 damage. Returns nothing. ```lua exports.ml_clothing:OnPedScreenPosition(data) exports.ml_clothing:OnPedZoom(data) ``` Drive the placement and the zoom of the preview ped shown next to the inventory. `OnPedScreenPosition` takes `{ screenX, screenY, screenH }` and ignores a call without both coordinates. `OnPedZoom` takes `{ delta }` and keeps the zoom between 0.6 and 1.6. Both return nothing. `OnTargetPedScreenPosition` is accepted for compatibility and has no effect. ```lua exports.ml_clothing:MLAdminPanel() exports.ml_clothing:MLAdminOpen() ``` `MLAdminPanel` returns `{ label, icon }`, the entry an admin hub needs to list the panel. `MLAdminOpen` asks the server to open it for the calling player and returns nothing. ```lua exports.ml_clothing:UseGizmo(itemData, propEntity, positionData, onComplete) exports.ml_clothing:StopGizmo() exports.ml_clothing:IsGizmoActive() ``` Start, stop and query the placement gizmo the admin panel uses to position a prop. `UseGizmo` does nothing while a gizmo is already running. `IsGizmoActive` returns a boolean. ## Item placement client hook To connect another provider, edit the three functions in `open/item_placement.lua`. This file is excluded from Asset Escrow. The supplied hook uses: **open/item_placement.lua** ```lua Micio.ItemPlacement = { isAvailable = function() return GetResourceState('ml_itemthrowing') == 'started' end, prepare = function(data) local ok, item = pcall(function() return lib.callback.await('ml_itemthrowing:prepareInventoryDrop', false, data) end) if not ok then return 'unavailable' end if type(item) ~= 'table' then return 'denied' end return 'ready', item end, start = function(item) exports.ml_itemthrowing:startGiveMode(item.name, item.slot, item.count, item.sourceInv, item.pendingToken) end, } ``` Adapt the arguments and return values to your provider's API. `isAvailable` must return a boolean. `false` uses a normal drop. Errors and other return types block the request. This check must not remove items or create world objects. `prepare` must authorize the request without removing items. Return `ready` with a table, `denied`, or `unavailable`. Only `unavailable` with no inventory or world changes permits a normal drop. Use `denied` for rejected or uncertain requests. Errors, unknown statuses and malformed `ready` payloads block the request. ```lua local request = { fromSlot = sourceSlot, toSlot = targetSlot, fromType = sourceInventory, toType = 'newdrop', count = quantity, rotated = rotated, } ``` `fromType` is `player` or a secondary inventory ID, including an equipped bag. Resolve the item, coordinates and routing bucket on the server; do not trust the optional item name. `start` receives the table returned by `prepare` after the inventory closes. Its return value is ignored. If closing fails, `start` is not called. A `start` error never triggers a normal drop. The provider server must validate inventory access, slot, integer quantity and item metadata. Bind pending requests to the player and source item, expire them, and reject repeated confirmations. On confirmation, recheck distance, routing bucket and item state. Remove the item only after confirmed placement; cancellation must keep it in its source inventory. Restart the inventory after editing the hook. ## Owner hooks `open/server.lua` ships unencrypted and is meant to be edited. Keep all four functions declared and change their bodies: an empty body turns a hook off. **open/server.lua** ```lua OpenServer = {} function OpenServer.PunishPlayer(source, reason) DebugPrint('[PUNISH]', 'Player %s: %s', source, reason) end function OpenServer.CanSearchPlayer(src, targetId) return true end function OpenServer.OnGearBroken(src, itemName, slot, key) end function OpenServer.OnGearRepaired(src, slot) end ``` - `PunishPlayer(source, reason)`: called when the server rejects a malformed plate carrier registration. Put the ban, kick or log here. The shipped body prints a debug line. - `CanSearchPlayer(src, targetId)`: called before `src` opens the bag or plate carrier worn by `targetId`, after the server has checked that both are connected and within 5 metres. Return `false` to refuse, for a dead, cuffed or unwilling target. Defaults to `true`. - `OnGearBroken(src, itemName, slot, key)`: fires once when a worn piece reaches zero durability. `key` is `c:::` for a component and `p:::` for a prop. - `OnGearRepaired(src, slot)`: fires when a player uses a repair item on a worn piece. A repair done through the `RepairGear` export does not fire it. > **INFO:** Notifications > > Notifications from ML Clothing are sent through [ml_bridge](https://portal.cfx.re), which picks the notification system the server runs. ## Appearance adapters `open/adapters/` holds one file per appearance format. `standard.lua` covers the format ML Clothing itself produces, `bl_appearance.lua` covers the `drawables` and `props` shape used by that family of appearance scripts. Each file defines a function on `Config.DataAdapters` that takes the appearance data and returns entries of `{ type, id, drawable, texture }`, where `type` is `component` or `prop`. When an outfit arrives the server walks `Config.DataAdapters` and keeps the first adapter that returns entries. The walk order is not fixed, so two adapters must never both accept the same payload. A new adapter returns an empty table for anything it does not recognise. Add the file to the folder, then `refresh` and restart the resource. A restart on its own can skip a file added since the last index. ## State bags ```lua if Player(source).state['micio:disable:give'] then return end ``` `micio:disable:` is set on the player and replicated, one key per feature name, `true` while the feature is off and absent otherwise. It mirrors `mlDisableFeature` so a client script can read the block without a server round trip. ```lua if LocalPlayer.state.micioInvOpen then return end ``` `micioInvOpen` is local to the player's own machine, `true` while the interface is open. ```lua local broken = LocalPlayer.state.ml_clothingBroken ``` `ml_clothingBroken` is set by ML Clothing on the player and replicated. It holds the worn pieces currently at zero durability, keyed `c:::` for components and `p:::` for props, and is cleared when nothing is broken. `ml_radiation` reads it to drop the protection of a worn out mask or suit when its `RespectWear` option is on. ## Events ```lua RegisterNetEvent('ox_inventory:lootRevealed', function(payload) print(payload.id, payload.type, payload.key) end) ``` - `ox_inventory:lootRevealed`, client. Payload `{ id, type, key }`, where `type` is `slot` or `cell` and `key` is the slot number or the cell id. Sent to every player who has that inventory open, when a slot or a cell is uncovered. - `micio:itemMetaChanged`, client, `(name)`. Sent to every player when an item definition is changed from the admin panel. ## Examples **Block an item from being destroyed** ```lua exports.ox_inventory:mlRegisterHook('beforeItemDelete', function(ctx) if ctx.itemName ~= 'evidence_bag' then return end return 'Evidence cannot be destroyed' end) ``` **Register a bag stash for a custom container** ```lua local uniqueId = 'trunk_' .. plate local slots, maxWeight, cols = 20, 40000, 5 local stashId = exports.ml_clothing:RegisterEquipmentStash('bag', uniqueId, slots, maxWeight, cols) ``` **Block writing into reserved clothing slots** ```lua local reserved local function isReserved(slot) if not reserved then local ok, list = pcall(function() return exports.ml_clothing:GetReservedSlots() end) reserved = ok and list or {} end for i = 1, #reserved do if reserved[i] == slot then return true end end return false end ``` **Repair worn gear from another resource** ```lua if exports.ml_clothing:IsGearBroken(src, slot) then exports.ml_clothing:RepairGear(src, slot, 100) end ``` **React to a piece breaking** **open/server.lua** ```lua function OpenServer.OnGearBroken(src, itemName, slot, key) TriggerClientEvent('myscript:gearBroken', src, itemName, key) end ```