How Integrations Work#
Nothing inside Appearance knows which framework, target or inventory you run. Every one of them sits behind a small bridge file, and the right bridge is picked at boot by looking for started resources.
Layer | Supported | Without it |
|---|---|---|
Framework |
| The resource stops with an error. There is no standalone mode. |
Target |
| Shops use a zone and an [E] prompt instead. That is the default. |
Inventory |
| Players can open their inventory while the clothing menu is up. Nothing else changes. |
Notifications |
| Falls back to ox_lib notifications and progress bars. |
Text prompt |
| Falls back to ox_lib's TextUI, then to 3D text drawn in the world. |
Tattoos |
| Appearance runs its own tattoo shops from config/tattoos.lua. With Tattoo Creator installed, its runtime tattoos live on top of them. |
Custom clothing |
| Clothing you publish with Clothing Creator is found by the pack scanner like any other add-on pack. Nothing to configure. |
Photography |
| You cannot photograph your own add-on clothing. The shipped catalogue is unaffected. |
Weather |
| Only used during a photo run, to freeze time and weather. Freeze it by hand or photos will vary. |
Every optional integration degrades silently. A missing resource makes its feature fall back to the built-in behaviour; it never errors and never blocks a start.
Overriding Detection#
Detection reads the list of started resources and takes the first match. Both lists sit near the top of config/main.lua and both can be replaced with a fixed value.
-- config/main.lua — auto, the default
local frameworks = {
['es_extended'] = 'esx',
['qb-core'] = 'qb',
['qbx_core'] = 'qb'
}
Config.Framework = DependencyCheck(frameworks) or 'none' -- [AUTO]
local inventories = { ['qs-inventory'] = 'qs-inventory' }
Config.Inventory = DependencyCheck(inventories) or 'standalone' -- [AUTO]
-- Forced: skip detection entirely
Config.Framework = 'esx'
Config.Inventory = 'standalone'This is worth doing when a resource has been renamed on your server, or when two candidates are running at once — detection walks the table unordered, so with two cores started the winner is genuinely unpredictable.
Frameworks#
Where a saved appearance ends up depends on the framework:
Framework | Appearance is stored in | Keyed on |
|---|---|---|
ESX | The skin column of the ESX users table |
|
QBCore / Qbox | playerskins, one active row per ped model |
|
On QB a character holds one saved look per ped model, with exactly one marked active. Switch model and switch back, and the earlier look for that model returns. ESX keeps a single blob with no per-model variants.
Multi-character#
Appearance keys everything on whatever identifier your framework hands it, so multi-character works with no extra setup — as long as that identifier is per character. ESX multicharacter prefixes and QB citizenids both are. A plain ESX install that keys users on the raw license gives every character on that license the same appearance, and nothing in Appearance can tell them apart.
A framework that is not on the list#
Supporting your own core is deliberately small: two files, both readable and editable even on an escrowed build. Duplicate custom/esx/ to custom/<yourcore>/ and work from there.
Each file returns a table. types.lua lists exactly which functions the server and client sides must implement — identifiers, money accounts, items, job name and grade, the admin check, and loading and saving an appearance. Then register the folder:
-- config/main.lua
local frameworks = {
['es_extended'] = 'esx',
['qb-core'] = 'qb',
['qbx_core'] = 'qb',
['my_core'] = 'mycore', -- loads custom/mycore/{client,server}.lua
}The last piece: the client side of the bridge must set the global PlayerData and call InitAppearance() once your core says the player is loaded. Everything else follows from there.
The whole of custom/, config/, locales/, types.lua and the provider folders stay readable and editable on an escrowed build, by design.
Clothing Is Not an Item#
Worth stating plainly, because it is the question that comes up most. Appearance does not turn garments into inventory items and registers no clothing items with any inventory. The inventory integration does exactly one thing: it stops a player opening their inventory while the clothing menu is open.
Money is touched only at checkout, through your framework's cash, bank or black-money accounts. To support another inventory, copy client/custom/inventory/standalone.lua, guard it on your own name, implement ToggleInventoryClothing(state), and add the resource to the inventories table.
Drop-in Compatibility#
Appearance answers the public surface of the three scripts it most often replaces, so your existing scripts do not have to be rewritten. These are compatibility shims — write new code against the real API — but old code keeps working.
illenium-appearance
Every export Appearance registers is also registered under the illenium-appearance name, and its outfit menu events are answered.
-- all of these keep working unchanged
exports['illenium-appearance']:getPedAppearance(PlayerPedId())
exports['illenium-appearance']:startPlayerCustomization(cb, cfg)
TriggerEvent('illenium-appearance:client:openOutfitMenu')
TriggerEvent('illenium-appearance:client:openJobOutfitsMenu')
TriggerServerEvent('illenium-appearance:server:saveAppearance', skin)An illenium-style menu config passed to startPlayerCustomization is translated automatically, including its propConfig aliases.
esx_skin and skinchanger (ESX only)
TriggerEvent('skinchanger:getSkin', function(skin) print(skin.model) end)
TriggerEvent('skinchanger:loadSkin', skin)
TriggerEvent('skinchanger:loadClothes', nil, clothes) -- legacy tshirt_1 keys
TriggerEvent('esx_skin:openSaveableMenu', onSubmit, onCancel)
exports['skinchanger']:GetSkin()
exports['skinchanger']:LoadSkin(skin)
exports['skinchanger']:LoadClothes(nil, clothes)The legacy flat skin format is converted on the way in, so a job script that dresses a player with tshirt_1-style keys still works.
qb-clothing and qb-clothes (QB only)
TriggerEvent('qb-clothing:client:openMenu') -- the new-character creator
TriggerEvent('qb-clothes:client:CreateFirstCharacter')
TriggerEvent('qb-clothing:client:openOutfitMenu')
TriggerEvent('qb-clothing:client:loadOutfit', { outfitData = data, name = 'Uniform' })Appearance also fires qb-clothing:client:onMenuClose when its menu closes, and reacts to qb-multicharacter:client:chooseChar by clearing tattoos and the cached outfits.
Migrating From Another Clothing Script#
Saved appearances carry across with one command. Run it once, then remove the old resource and the admin group that could run it.
Coming from | Command | Needs |
|---|---|---|
|
| group.admin, ESX |
|
| group.god, QB/Qbox |
|
| group.god, QB/Qbox |
|
| group.admin |
|
| group.admin |
Appearance 1.x skins |
| Server console, or the command.convert ace |
/migrateskins means two different things
Only one version of the command is ever loaded, decided by your framework, and their signatures differ. On ESX it takes no argument and converts the skin column of every row in users that still looks like an old esx_skin blob. On QB it requires the source as an argument and sits behind group.god, not group.admin.
/migrateskins qb-clothing runs through your own client
Old qb-clothing rows store the ped model as a hash, and the only reliable way to read one back is to wear it. The command applies each affected skin to the admin who ran it, one at a time, reads the result and saves it in the new format. Stay in game and leave the window focused until it finishes. Rows whose model is already a name are skipped and reported.
/migrate_rcore empties playerskins first on QB
On QBCore the command deletes the whole playerskins table before inserting the converted rcore appearances. On ESX it updates users.skin per identifier instead. The command's own help text says to take a backup; take one.
Ped models it cannot resolve fall back to mp_m_freemode_01 with a console warning, so start the resources that stream your custom peds before running it.
illenium outfits usually need no migration
illenium-appearance already keeps its outfits in player_outfits, the same table Appearance reads, in a compatible shape. In most cases players simply find their outfits waiting for them. Run /migrateoutfits only if they do not appear — and remember that importing [sql]/main.sql on an existing server drops that table, which is the usual reason outfits vanish during a migration.
Where Catalogue Pictures Come From#
Each catalogue row stores the address of its own picture. The roughly 28,900 shipped rows point at Quasar's CDN and are already photographed, so a new server needs no image hosting of any kind.
Hosting only becomes a question for clothing you add yourself. When you photograph an add-on pack, the picture is cropped in the interface and uploaded to FiveManage using the token in config/fivemanage.lua, and the returned address is written into the catalogue row. A placeholder token ships in the file — replace it with your own before the first run.
Three helper pieces ship alongside the script. greenscreen-map is the studio the capture teleports to, and is the only one most owners ever start. clothing-image-saver and clothing-ai-service are vendor tooling used to build the shipped catalogue — a local image host and an offline labelling service. Neither is needed to run a server, and the AI block in the config is marked development-only and ships disabled.
Languages#
41 translations ship in locales/: Arabic, Bulgarian, Bengali, Catalan, Czech, Danish, German, Greek, English, Spanish, Estonian, Persian, Finnish, French, Hebrew, Hindi, Croatian, Hungarian, Indonesian, Italian, Japanese, Korean, Lithuanian, Latvian, Malay, Dutch, Norwegian, Polish, Portuguese, Romanian, Russian, Slovak, Slovenian, Swedish, Swahili, Thai, Turkish, Ukrainian, Urdu, Vietnamese and Chinese.
Config.Locale = 'es' -- the filename in locales/, without .jsonA missing file falls back to English with a console warning, and so does any individual key a translation has not caught up with — so a half-finished custom translation never shows a blank label. It shows the English one and tells you which key to add.



