Skip to content

lumo-clothing — feature reference ​

Everything here is configured in shared.lua and can be switched off there. The database tables are created automatically on first start.

Job lockers ​

Job changing rooms are configured in shared.lua under Lumo.JobLockers and Lumo.JobUniforms. Each location has a visible attendant ped; walk within two metres as the matching employee and press E on the Lumo prompt. A locker provides:

  • grade-aware male/female duty uniforms;
  • the player's existing Lumo saved outfits; and
  • restoration of the personal clothing worn before a temporary duty uniform.

Default test locations:

  • Police: vector4(454.37, -993.23, 30.69, 357.75) (MRPD locker room)
  • EMS: vector4(341.93, -586.24, 43.32, 339.41) (Pillbox)
  • City mechanic: vector4(-345.60, -123.08, 39.01, 239.77)
  • Harmony mechanic: vector4(1186.15, 2636.62, 38.40, 283.56)
  • Airport mechanic: vector4(-1139.92, -2005.77, 13.18, 142.70)
  • Benny's: vector4(-201.87, -1314.76, 31.09, 181.40)
  • Beeker's: vector4(99.92, 6621.45, 32.44, 139.65)

Prompts use lumo-interact when it runs, otherwise ox_target / qb-target, otherwise a built-in [E] prompt. Job lockers need a job, so on a standalone server they work only if you provide one (Standalone.GetJob in bridge/config.lua).

Clothing ownership (Lumo.Ownership) ​

Buying a garment records it in lumo_wardrobe against the character. Re-buying something already owned costs nothing and the card reads Owned instead of a price. A character's existing outfit is grandfathered in the first time they connect, so nothing is taken away on the day this ships.

strictOutfits (off by default) additionally strips components a character does not own out of a saved outfit when they wear it.

lua
exports['lumo-clothing']:OwnsGarment(citizenid, gender, props)  -- server

Condition & laundry (Lumo.Laundry) ​

Worn garments gain dirt over time and damage when the character is hurt, dies, or is revived. Both are 0-100 per garment; the panel shows a strip once the outfit reads scruffy. Laundromats are world prompts: E to wash (priced per dirty garment plus a mending charge), G to check condition.

lua
exports['lumo-clothing']:GetClothingCondition()   -- client, this player
exports['lumo-clothing']:IsPresentable()          -- client, quick gate for jobs
exports['lumo-clothing']:GetWornSignatures()      -- client

Gate a job interaction on it:

lua
if not exports['lumo-clothing']:IsPresentable() then
    Notification('You are not dressed for this.', 'error')
    return
end

Brand tiers (Lumo.Brands) ​

Items carry a brand name, a tier (1 Discount … 5 Couture) and a knockoff flag, all editable in the Item Editor. Each shop has a margin multiplier its owner sets under Admin → Manage Shops → Pricing; the listed price is struck through when a margin is applied. Margins are clamped to marginMin/marginMax and enforced server-side, so the panel can never be talked into a cheaper price.

Clothing as evidence (Lumo.Evidence) ​

On-duty police within 8m can take a description of what someone is wearing. A worn mask flags the record as concealed and omits the citizenid — masks are what make this mechanic worth anything.

/describe            -- nearest player
/describe [id]       -- specific player
/describe [id] [ref] -- tag it to an incident reference
lua
exports['lumo-clothing']:DescribePlayer(serverId, incident)          -- client
exports['lumo-clothing']:SearchClothingEvidence(filters, cb)         -- client
exports['lumo-clothing']:RecordClothingEvidence(payload)             -- server

Records live in lumo_clothing_evidence and are pruned after Lumo.Evidence.retainDays. The description is reported by the described player's client, which is the only place the live ped state exists — treat it as a witness statement, not a forensic fact.

Property wardrobes (Lumo.PropertyWardrobe) ​

lua
exports['lumo-clothing']:OpenPropertyWardrobe(propertyId, { inside = true })
local id = exports['lumo-clothing']:AddPropertyWardrobePoint(propertyId, coords, 'Wardrobe')
exports['lumo-clothing']:RemovePropertyWardrobePoint(id)

lumo-housing already routes its wardrobe furniture here via client/custom/wardrobe/lumo-clothing.lua.

Clothing as inventory items (Lumo.ClothingItems) ​

One inventory item per wearable slot (clothing_jacket, clothing_mask, …) plus hairtie. The garment itself — drawable, texture, brand, tier, condition and a per-garment serial — travels in the stack's metadata, so a catalog of thousands needs only thirteen item definitions (install/qb_items.lua or install/ox_inventory_items.lua). This needs an inventory with per-item metadata (ox_inventory, qb-inventory, lumo-inventory, qs, ps); ESX's own inventory cannot hold metadata, so on ESX use ox_inventory for this feature (with ESX's inventory, clothes simply stay on).

Taking something off hands it to you; using it puts it back on. Because they are ordinary inventory items, garments can be traded, stashed, sold or stolen.

/takeoff              -- pick from what you are wearing
/takeoff jacket       -- straight to one slot
/wear                 -- pick from the garments you are carrying
/wear jacket          -- only show that slot
/jacket /tshirt ...   -- one command per slot: off if worn, back on if carried

Every slot name is resolved through Lumo.ClothingItems.aliases, so /top, /torso and /jacket are the same slot, and so are /shirt, /tshirt and /undershirt. Names another started resource owns (qb-radialmenu's /shirt, /pants, …) are listed in reservedCommands and skipped here — that resource routes into this API instead, so a command is never registered twice.

Why it cannot be duped: every conversion is checked against the character's saved skin server-side, and the slot is cleared in the same step the item is created. A client can only bank a garment the server can already see them wearing, and only once. If the item cannot be created (no room), the slot is restored before anything is lost. Wearing a garment cut for the other body type is refused — the same drawable id is a completely different garment on the other freemode model.

Swapping is safe: putting a jacket on while wearing one banks the old jacket first, so nothing is destroyed. Taking a jacket off also banks the sleeves (component 3) with it and restores them when it goes back on — otherwise a bare torso is left wearing the coat's cuffs.

One wardrobe API, every entry point ​

Clothing can be taken off from several places: this resource's commands and menus, qb-radialmenu's clothing wheel, and qb-inventory's character panel. They all call the API below rather than writing ped components themselves, so there is exactly one source of truth — the saved skin — and every route produces the same item.

lua
-- client
exports['lumo-clothing']:ToggleSlot('top')      -- off if worn, on if carried
exports['lumo-clothing']:TakeOffSlot('jacket')
exports['lumo-clothing']:WearSlot('jacket')
exports['lumo-clothing']:OpenTakeOffMenu()
exports['lumo-clothing']:OpenWearMenu('jacket') -- omit the slot for everything
exports['lumo-clothing']:HandlesSlot('gloves')  -- false: not a garment slot
exports['lumo-clothing']:ResolveSlot('chain')   -- 'neck'

-- server
exports['lumo-clothing']:ToggleGarment(src, 'top')
exports['lumo-clothing']:TakeOffGarment(src, 'jacket')
exports['lumo-clothing']:WearGarmentFromInventory(src, 'jacket')
exports['lumo-clothing']:GiveGarmentItem(src, 'jacket', 47, 2)
exports['lumo-clothing']:IsWearing(src, 'jacket')
exports['lumo-clothing']:ResolveSlot('Top')     -- 'jacket'

Net events (for menu params.event entries) and callbacks:

lumo-clothing:server:toggleSlot     <slot>
lumo-clothing:server:takeOff        <slot>
lumo-clothing:server:wearSlot       <slot>
lumo-clothing:server:wearItemSlot   <inventory slot>
lumo-clothing:client:openTakeOff
lumo-clothing:client:openWear
lumo-clothing:client:toggleSlot     { slot = 'jacket' }

callback lumo-clothing:server:slotState        -- worn / carried per slot
callback lumo-clothing:server:carriedGarments  -- garments in the inventory
callback lumo-clothing:server:wearableSlots    -- what can be taken off now

HandlesSlot is what makes an adapter safe to write: it is false for anything that is not a garment (gloves, visors) and false when Lumo.ClothingItems is switched off, so the calling resource can fall back to its own behaviour.

Admin: /giveclothing [id] [slot] [drawable] [texture] (slot accepts aliases)

Hair fit — hair ties and the hat clipping fix (Lumo.Hair) ​

GTA decides how much hair a head prop cuts from pedalternatevariations.meta inside the ped's DLC. Vanilla hair is described there; add-on and EUP hair usually is not, which is why it pokes through hats and helmets. No runtime native rewrites that metadata, so the working fix — the one every headgear script uses — is to swap the hair drawable for a shorter one while a head prop is worn, and swap it back afterwards.

  • hairtie item (or /hairtie) toggles hair tied back, with a progress bar.
  • autoFitUnderHats does the same automatically the moment a head prop goes on, and undoes it when the prop comes off.

The swap is applied to the ped only. The character's real hair stays in skinData, so nothing here can permanently restyle anyone, and a save while fitted still stores the real hairstyle.

Map your clipping hair in Lumo.Hair.tied[gender] as [hairDrawable] = tiedDrawable; fallback covers anything listed in clipsUnderHats with no mapping of its own. Hair with no entry is left completely alone, so an unconfigured server behaves exactly as it does today.

lua
exports['lumo-clothing']:ToggleHairTie()   -- client
exports['lumo-clothing']:IsHairTied()      -- client
exports['lumo-clothing']:RefitHair()       -- client, after you change hair yourself

Laundromat ​

The default laundry point is the free Jerry's Laundromat MLO by levdevlev (MIT): https://github.com/levdevlev/lev-laundromat. Install it as lev_laundromat and start it before lumo-clothing. Without the map the laundry prompt and blip simply stay hidden; add your own points in Lumo.Laundry.locations. Details: Laundry.