Clothing Integration
Clothing Integration
ML Clothing keeps worn clothes and inventory items in sync in two directions. Saving worn clothing back to the appearance script is automatic. The resource detects the appearance script at startup and writes to it when a clothing item is equipped or unequipped, and when the inventory closes. The opposite direction, turning an outfit picked in an appearance menu into inventory items, needs one line added to the appearance script at every point where an outfit is saved.
Worn clothing is written back into illenium-appearance, bl_appearance, 4bit_appearance and codem-appearance (also detected under its other name, crm-appearance). fivem-appearance and qb-clothing are detected as well, but nothing is written back into them: on those two the outfit lives in the inventory and in the appearance script's own storage independently. When no appearance script is running, the outfit lives in the inventory only, and the sync entry points still bring an outfit in.
Sync entry points
Call either of these from the appearance script whenever a player saves or changes an outfit. Both are client side. From server code, route through your own client event first.
1exports.ml_clothing:syncFromNUI(appearanceData)1TriggerEvent('ml_clothing:client:syncFromNUI', appearanceData)Both send the appearance data to the server, which translates it through the data adapters and places the matching clothing items into the reserved inventory slots. The sync makes the clothing slots match the outfit exactly. An item in a slot the new outfit does not set is removed from the inventory, not moved to a free cell.
When the appearance data of the script is not available or its format has no adapter, read the live ped instead. The call below captures the current components and props after the appearance script has applied them:
1exports.ml_clothing:syncFromPed()The server drops syncs that arrive during the first seconds after a character loads, while the inventory is still rebinding. A dropped sync prints syncNewOutfit blocked in the server console when Config.Debug = true.
Trust model
A sync carries whatever the appearance script hands over, so that script's save flow is the trust boundary. The server keeps the payload inside fixed limits before anything is written:
- The load gate above.
- One sync per player per second.
- One sync at a time per player.
- An entry whose drawable falls outside 0 to 65535 or whose texture falls outside 0 to 255 is dropped, not corrected. Inside those bounds the value is taken as sent, so a drawable the ped model does not have is passed through.
- An entry is ignored unless its component or prop id maps to a known clothing item.
- That item is placed only in the slot its own definition names.
A component whose drawable and texture match the default appearance for the character's sex produces no item and leaves the slot empty.
Data adapters
Each appearance format has its own translator file in open/adapters/. The adapters in the folder:
standard.luareadscomponentsandpropsentries built fromcomponent_idorprop_id,drawableandtexture. This is the default shape and it coversillenium-appearance.bl_appearance.luareadsdrawablesandpropsentries built fromindex,valueandtexture.
An adapter turns the appearance payload into entries of { type, id, drawable, texture } where type is component or prop, and returns an empty table when the payload is not its format. The server walks the registered adapters and takes the first one that returns entries. The order is not guaranteed, so two adapters must never both match the same payload. If a custom payload also carries components, edit standard.lua to narrow it or delete the file, so only one adapter can claim the payload.
To support a different appearance script, drop a new file in the folder and call one of the sync entry points from that script:
1Config.DataAdapters = Config.DataAdapters or {}
2
3function Config.DataAdapters.custom_appearance(appearanceData)
4 local standardClothes = {}
5
6 return standardClothes
7endConfig.DataAdapterstable of adapter functions, default{}in a new adapter file and filled by every adapter file in the folder. Each key is the adapter name, each value the adapter function.
A file added to the resource is only picked up after refresh followed by ensure ml_clothing. A plain restart without refresh skips new files.
illenium-appearance
Three functions in client/client.lua save an outfit. Add the export call right after each illenium-appearance:server:saveAppearance trigger.
Character creation
Covers the starting clothes of a fresh character. Without this call the outfit picked during character creation never becomes inventory items.
1client.startPlayerCustomization(function(appearance)
2 if (appearance) then
3 TriggerServerEvent("illenium-appearance:server:saveAppearance", appearance)
4 exports.ml_clothing:syncFromNUI(appearance) -- add this line
5 if onSubmit then
6 onSubmit()
7 end
8 elseif onCancel then
9 onCancel()
10 end
11end, config)Clothing shop
Inside OpenShop, in the customization callback:
1client.startPlayerCustomization(function(appearance)
2 if appearance then
3 if not isPedMenu then
4 TriggerServerEvent("illenium-appearance:server:chargeCustomer", shopType)
5 end
6 TriggerServerEvent("illenium-appearance:server:saveAppearance", appearance)
7 exports.ml_clothing:syncFromNUI(appearance) -- add this line
8 end
9 Framework.CachePed()
10end, config)Outfit change
In the handler for illenium-appearance:client:changeOutfit, in the branch that saves:
1else
2 local appearance = client.getPedAppearance(cache.ped)
3 TriggerServerEvent("illenium-appearance:server:saveAppearance", appearance)
4 exports.ml_clothing:syncFromNUI(appearance) -- add this line
5endcodem-appearance
Saving worn clothing back into codem-appearance is automatic: after a clothing item is equipped or unequipped, and when the inventory closes, ML Clothing calls the crm_save_appearance export, which reads the live ped and writes its own row. Nothing to set up for that direction.
The other direction needs no edit to the appearance script: opening the inventory reads the live ped and syncs it, so an outfit picked in its menus lands in the clothing slots at the next inventory open. To get an outfit into the slots sooner, call syncFromPed from your own code once the appearance has been applied to the ped:
1exports.ml_clothing:syncFromPed()4bit_appearance
Worn clothing is written back into 4bit_appearance automatically. The outfit to items direction is not automatic and has no inventory-open fallback: call exports.ml_clothing:syncFromPed() from your own code after its menu has applied the appearance to the ped.
bl_appearance
The release ships built, with no src/. Apply the two edits below to the deployed dist/client/init.js, or make them in a source checkout and rebuild with its own build script. The file names below are the source paths; the same two calls are in the built file.
In src/client/appearance/setters.ts, as the last line of setPlayerPedAppearance:
1export async function setPlayerPedAppearance(data) {
2 setPedHairColors(ped, data.hairColor)
3 setPedTattoos(ped, data.tattoos)
4 emit("ml_clothing:client:syncFromNUI", data) // add this line
5}In src/client/handlers.ts, inside the save callback, after the save round trip has produced the validated appearance:
1RegisterNuiCallback(Receive.save, async (appearance, cb) => {
2 const newAppearance = await getAppearance(ped)
3 newAppearance.tattoos = appearance.tattoos || null
4 triggerServerCallback("bl_appearance:server:saveAppearance", getFrameworkID(), newAppearance)
5 emit("ml_clothing:client:syncFromNUI", newAppearance) // add this line
6 setPedTattoos(ped, newAppearance.tattoos)
7 closeMenu()
8 cb(1)
9})Placing the emit at the top of setPlayerPedAppearance sends the raw menu payload. Out of range drawables from presets or missing addon packs would then be written into item metadata and spam SetVariation errors at the next login. Keep the emit after the setters have run, as the last line of the function.
Verify
1Config.Debug = falseConfig.Debugdefaultfalse. Set it totruefor the console prints used below, back tofalseonce the check is done.
With debug on:
- Restart
ml_clothingafter editingshared/config.lua, and restart the appearance script after adding the export call. Forbl_appearance, restart it after the rebuild or after editing the deployed bundle. - Change outfit in the appearance menu and save.
- The clothing slots in the inventory update to the new outfit and the player gets the outfit updated notification. Nothing is printed on success: the console only speaks when a sync is refused.
The notification fires whenever the sync is accepted, including when no adapter recognised the payload. The clothing slots are the only proof the adapter matched: if they do not change, the payload reached the server and no adapter claimed it.
If the console prints syncNewOutfit blocked instead, the player was not fully loaded yet; wait a few seconds in game and save again. The gate opens once the inventory and the character id read stable after the character loads. If it never opens, the console names the reason with Config.Debug = true: the inventory was not stable within ten seconds, or a character switch was detected. Rejoining reopens the gate.