Clothing Integration

5 min readUpdated 2 weeks ago

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 running appearance script at startup and writes through its save path. 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. This page shows where to add that line for illenium-appearance and bl_appearance, and how to adapt any other appearance script.

Sync entry points

Call either of these from the appearance script whenever a player saves or changes an outfit. Both send the appearance data to the server, which translates it through the data adapters and places the matching clothing items into the dedicated inventory slots.

lua
1exports.ml_clothing:syncFromNUI(appearanceData)
lua
1TriggerEvent('ml_clothing:client:syncFromNUI', appearanceData)

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, so it works with any appearance system:

lua
1exports.ml_clothing:syncFromPed()
Load gate

The server drops syncs that arrive during the first seconds after a character loads, while the inventory is still rebinding. This protects against item corruption on login and character switch. A dropped sync prints syncNewOutfit blocked in the server console when Config.Debug = true. Normal menu flows finish well after the gate opens.

Data adapters

The server does not care which appearance script produced the data. Each shipped format has its own translator file in open/adapters/: illenium.lua, bl_appearance.lua, and native.lua. An adapter turns the appearance payload into entries of { type, id, drawable, texture } where type is component or prop; the first adapter that returns entries wins. To support a different appearance script, drop a new file in the folder and call one of the sync entry points from that script:

open/adapters/custom_appearance.lua
1Config.DataAdapters = Config.DataAdapters or {}
2
3function Config.DataAdapters.custom_appearance(appearanceData)
4    local standardClothes = {}
5    -- translate the outfit data into entries like:
6    -- standardClothes[#standardClothes + 1] = { type = 'component', id = 11, drawable = 15, texture = 0 }
7    return standardClothes
8end
New files need a refresh

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. Unchanged lines are collapsed with -- ....

Character creation

Covers the starting clothes of a fresh character. Without this call the outfit picked during character creation never becomes inventory items.

client/client.lua
1function InitializeCharacter(gender, onSubmit, onCancel)
2    -- ...
3    client.startPlayerCustomization(function(appearance)
4        if (appearance) then
5            TriggerServerEvent("illenium-appearance:server:saveAppearance", appearance)
6            exports.ml_clothing:syncFromNUI(appearance) -- add this line
7            if onSubmit then
8                onSubmit()
9            end
10        elseif onCancel then
11            onCancel()
12        end
13        -- ...
14    end, config)
15end

Clothing shop

client/client.lua
1function OpenShop(config, isPedMenu, shopType)
2    lib.callback("illenium-appearance:server:hasMoney", false, function(hasMoney, money)
3        -- ...
4        client.startPlayerCustomization(function(appearance)
5            if appearance then
6                if not isPedMenu then
7                    TriggerServerEvent("illenium-appearance:server:chargeCustomer", shopType)
8                end
9                TriggerServerEvent("illenium-appearance:server:saveAppearance", appearance)
10                exports.ml_clothing:syncFromNUI(appearance) -- add this line
11            else
12                -- ...
13            end
14            Framework.CachePed()
15        end, config)
16    end, shopType)
17end

Outfit change

client/client.lua
1RegisterNetEvent("illenium-appearance:client:changeOutfit", function(data)
2    -- ...
3        if data.disableSave then
4            -- ...
5        else
6            local appearance = client.getPedAppearance(cache.ped)
7            TriggerServerEvent("illenium-appearance:server:saveAppearance", appearance)
8            exports.ml_clothing:syncFromNUI(appearance) -- add this line
9        end
10    -- ...
11end)

codem-appearance

Detection covers the resource under both of its common names, crm-appearance and codem-appearance. Saving worn clothing back to its database is automatic: after every inventory equip or unequip, 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.

Because the resource is escrowed, its files cannot be edited. Wire the outfit-to-items direction from the scripts on your side that call its documented hooks. Character creation, in the multicharacter script:

lua
1TriggerEvent('crm-appearance:init-new-character', 'crm-male', function()
2    exports.ml_clothing:syncFromPed()
3end)

Any custom flow that saves through the export:

lua
1exports['crm-appearance']:crm_save_appearance(nil, function()
2    exports.ml_clothing:syncFromPed()
3end)

The menus opened with crm-appearance:show-clothing-menu, show-outfits, and show-barber-menu do not expose a save callback, so there is no hook point for those flows. Outfits changed there reach the inventory the next time a flow with a callback runs, or when the player equips something through the inventory.

bl_appearance

bl_appearance is TypeScript. Edit the two spots below in src/client/ and rebuild with pnpm run init, or apply the same lines directly to the deployed dist/client/init.js.

In src/client/appearance/setters.ts, as the last line of setPlayerPedAppearance:

src/client/appearance/setters.ts
1export async function setPlayerPedAppearance(data) {
2    // ...
3    setPedHairColors(ped, data.hairColor)
4    setPedTattoos(ped, data.tattoos)
5    emit("ml_clothing:client:syncFromNUI", data) // add this line, keep it last
6}

In src/client/handlers.ts, inside the save callback, after the save round trip has produced the validated appearance:

src/client/handlers.ts
1RegisterNuiCallback(Receive.save, async (appearance, cb) => {
2    // ...
3    const newAppearance = await getAppearance(ped)
4    newAppearance.tattoos = appearance.tattoos || null
5    triggerServerCallback("bl_appearance:server:saveAppearance", getFrameworkID(), newAppearance)
6    emit("ml_clothing:client:syncFromNUI", newAppearance) // add this line
7    setPedTattoos(ped, newAppearance.tattoos)
8    closeMenu()
9    cb(1)
10})
Emit position

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.

Verify

  1. Set Config.Debug = true in shared/config.lua and restart ml_clothing.
  2. Change outfit in the appearance menu and save.
  3. The clothing slots in the inventory update to the new outfit and the server console logs the sync.

If the console prints syncNewOutfit blocked instead, the player was not fully loaded yet; wait a few seconds in game and save again.