Developer
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 two files a server owner edits.
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.
1local 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.
1exports.ox_inventory:mlRegisterHook('beforeItemDelete', function(ctx)
2 if ctx.itemName == 'evidence_bag' then
3 return 'Evidence cannot be destroyed'
4 end
5end)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 returningfalsestops the run and the action. A handler returning a string stops it and the string comes back as the reason. Returnsok, 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 returningfalsecancels. 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. Returningfalseor 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
1exports.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:OpenLootStashreturns 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
1exports.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.
1playerCols = 8,
2dropCols = 6,
3secondaryCols = 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:
1exports.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:
1exports.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
1local 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
1exports.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
1local 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.
1local 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
1exports.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.
1exports.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.
1exports.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.
1exports.ml_clothing:GetEquipmentStashId(stashType, uniqueId)Returns the id RegisterEquipmentStash would build for the same arguments, without registering anything.
1exports.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.
1exports.ml_clothing:IsGearBroken(src, slot)Returns true when the piece worn in that slot is currently flagged broken, otherwise false.
1exports.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
1exports.ml_clothing:getCurrentAppearance()Returns the current ped appearance, components and props.
1exports.ml_clothing:applyAppearance(appearanceData)Applies an appearance table to the local ped and returns nothing.
1exports.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.
1exports.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.
1exports.ml_clothing:clearSkin()Resets the ped clothing components to their default state. Returns nothing.
1exports.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.
1exports.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.
1exports.ml_clothing:lootTrapEffect()Plays the trap reaction on the local ped: animation, screen effect, sound and 25 damage. Returns nothing.
1exports.ml_clothing:OnPedScreenPosition(data)
2exports.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.
1exports.ml_clothing:MLAdminPanel()
2exports.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.
1exports.ml_clothing:UseGizmo(itemData, propEntity, positionData, onComplete)
2exports.ml_clothing:StopGizmo()
3exports.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.
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.
1OpenServer = {}
2
3function OpenServer.PunishPlayer(source, reason)
4 DebugPrint('[PUNISH]', 'Player %s: %s', source, reason)
5end
6
7function OpenServer.CanSearchPlayer(src, targetId)
8 return true
9end
10
11function OpenServer.OnGearBroken(src, itemName, slot, key)
12end
13
14function OpenServer.OnGearRepaired(src, slot)
15endPunishPlayer(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 beforesrcopens the bag or plate carrier worn bytargetId, after the server has checked that both are connected and within 5 metres. Returnfalseto refuse, for a dead, cuffed or unwilling target. Defaults totrue.OnGearBroken(src, itemName, slot, key): fires once when a worn piece reaches zero durability.keyisc:<component>:<drawable>:<texture>for a component andp:<prop>:<drawable>:<texture>for a prop.OnGearRepaired(src, slot): fires when a player uses a repair item on a worn piece. A repair done through theRepairGearexport does not fire it.
Notifications from ML Clothing are sent through ml_bridge, 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
1if Player(source).state['micio:disable:give'] then return endmicio:disable:<feature> 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.
1if LocalPlayer.state.micioInvOpen then return endmicioInvOpen is local to the player's own machine, true while the interface is open.
1local broken = LocalPlayer.state.ml_clothingBrokenml_clothingBroken is set by ML Clothing on the player and replicated. It holds the worn pieces currently at zero durability, keyed c:<component>:<drawable>:<texture> for components and p:<prop>:<drawable>:<texture> 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
1RegisterNetEvent('ox_inventory:lootRevealed', function(payload)
2 print(payload.id, payload.type, payload.key)
3end)ox_inventory:lootRevealed, client. Payload{ id, type, key }, wheretypeisslotorcellandkeyis 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
1exports.ox_inventory:mlRegisterHook('beforeItemDelete', function(ctx)
2 if ctx.itemName ~= 'evidence_bag' then return end
3
4 return 'Evidence cannot be destroyed'
5end)1local uniqueId = 'trunk_' .. plate
2local slots, maxWeight, cols = 20, 40000, 5
3
4local stashId = exports.ml_clothing:RegisterEquipmentStash('bag', uniqueId, slots, maxWeight, cols)1local reserved
2
3local function isReserved(slot)
4 if not reserved then
5 local ok, list = pcall(function() return exports.ml_clothing:GetReservedSlots() end)
6
7 reserved = ok and list or {}
8 end
9
10 for i = 1, #reserved do
11 if reserved[i] == slot then return true end
12 end
13
14 return false
15end1if exports.ml_clothing:IsGearBroken(src, slot) then
2 exports.ml_clothing:RepairGear(src, slot, 100)
3end1function OpenServer.OnGearBroken(src, itemName, slot, key)
2 TriggerClientEvent('myscript:gearBroken', src, itemName, key)
3end