Pack descriptor
Stability: Stable.
A cosmetic pack is a plain-data table passed to
registerPack. It declares a tab of items for the customization
screen, in one of three kinds:
'skin'— whole ped models. A skin'sidis its GTA ped model — a base-game ped for an OSS pack, or a model the pack streams for a proprietary one.'outfit'— streamed add-on clothing dressed onto a freemode base ped. An outfit'sidis an arbitrary equip id (not a model); the model is itsbase.'weaponTex'— weapon wraps; theidis a wrap name carrying atint.
Skins and outfits share the appearance axis: one equip slot, one player pref, one catalogue surface — a player wears a ped-model skin or an outfit, never both. Wraps are a separate axis with their own pref.
Authoring with definePack
Include the contract into your pack's VM to author with definePack{...} — it
validates at your pack's own load and gives editor autocomplete from the LuaCATS
types:
-- pack fxmanifest.lua
shared_scripts {
'@sdk/shared/kit.lua', -- SDK helpers the contract uses (load first)
'@sdk/shared/cosmetics.lua', -- definePack + the RfxPack types
}
-- pack.lua
local NEON = definePack{ id = 'neon', apiVersion = 2, kind = 'skin', skins = { ... } }definePack raises with diagnostics if the pack is malformed, so it fails loudly at
load. The no-include path is exports.sdk:definePack(spec).
Versioning
The pack contract has its own version, independent of the mode contract. Pin the version your pack was written against:
apiVersion = exports.sdk:packApiVersion() -- or hard-code the integer you targetedvalidatePack compares your apiVersion to the SDK's PACK_API_VERSION: a newer
target is refused (update the sdk resource); an older one warns.
Current version: 2. v1 → v2 added kind='outfit' and the item fields
base / collection / pieces (plus the pack-level dlcName). The change is
purely additive — a v1 skin/weaponTex pack is still valid and still loads; it
only warns to re-pin apiVersion = 2.
Top-level fields
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
id | string | yes | — | Unique pack id. |
apiVersion | integer | no | — | Cosmetic contract version you target. Pin it. |
kind | string | no | 'skin' | 'skin' (ped models) | 'outfit' (streamed clothing on a freemode ped, see below) | 'weaponTex' (weapon wraps — a global tint, see below). One registry, many kinds. |
dlcName | string | outfit | — | The pack's add-on clothing DLC name — the dlcName in its ShopPedApparel .meta, and the tail of its streamed .ymt. Every outfit item derives collection = <base>_<dlcName> from it, so the one name that must match the shipped assets is written once per pack. Lowercase letters/digits/underscore only (the streamed .ymt filename is all-lowercase; anything else warns). A per-item collection overrides it. |
status | string | no | 'owned' | 'owned' (everyone has it) | 'locked' (gated — unlocked by an active VIP subscription, or bought outright via sku). |
access | string | no | 'player' | 'player' | 'vip'. Presentation hint only — it does not gate. It labels how the pack is meant to be acquired; the ownership rule is the server's (ownsPack: free OR bought OR VIP). |
unlock | string | no | — | Unlock CTA text (locked packs). |
sku | string | no | — | Tebex package id — declares the pack standalone-purchasable (a one-off buy the player keeps for life, independent of VIP). The single source of truth for "this pack is for sale": the store/webhook map a paid sku → ownership of this pack id, and the NUI shows a Buy CTA. Omit for a free or VIP-only pack. Setting it does not remove the pack from VIP — the two routes coexist. |
present | table | advised | — | The pack's card identity + showcase — title / sub / rgb / desc (+ optional hero / info / buy), the SAME shared block a mode uses. present.title is the tab name. See Presentation block. |
skins | array | yes | — | The items (non-empty), whatever the kind — outfits and wraps live in skins too. See below. |
skins[] — the items
Each entry is a cosmetic item. What the id means depends on the pack kind: a
skin's id IS its GTA ped model; an outfit's id is an arbitrary equip id (the model
is base); a wrap's id is a wrap name. All are persisted in the rfx:pref:* KVP.
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | yes | The equipped id. kind='skin': the GTA model name. kind='outfit': a free-form id. kind='weaponTex': a wrap name. |
base | string | outfit | The freemode ped this outfit dresses: 'mp_m_freemode_01' or 'mp_f_freemode_01' — nothing else (hard error). See below. |
collection | string | outfit | The pack's ped-variation collection this item's pieces live in. Usually omitted — it derives from the pack-level dlcName as <base>_<dlcName>. Set it only for a pack streaming several collections. |
pieces | array | outfit | Non-empty array of { slot, drawable, texture?, prop? } — the garments. See below. |
label | string | advised | Card label (defaults to the id). |
rarity | string | no | 'common' | 'rare' | 'vip' | 'legend' (tick colour only). |
cat | string | no | Gender filter chip: 'Male' | 'Female' | 'Unisex'. For an outfit it is derived from base (male/female clothing catalogues are entirely separate assets) — omit it; a value that contradicts base is an error. |
locked | boolean | no | Grey ONE item inside an OWNED pack (e.g. a VIP piece). |
lock | string | no | Short tag shown on a locked item (e.g. 'VIP'). |
image | string | no | Explicit card thumbnail URL. Omit for a base-game ped and the runtime derives one from the assets base. Set it for a pack that hosts its own render. |
tint | integer | weaponTex | GTA weapon tint index applied to every weapon the player holds — the built-in wrap payload. See below. |
swatch | string | weaponTex | "r g b" card colour, since a built-in tint has no render (used when there's no image). |
Base-game vs streamed models (kind='skin')
The descriptor is agnostic of where the asset lives:
- OSS pack —
idis a base-game ped (a_m_m_skater_01,s_m_y_swat_01, …), already on every client; the pack streams nothing. Seecosmetic_pack_base. - Proprietary pack —
idis a model the pack streams from itsstream/folder, named to match the id. See thecosmetic_pack_neontemplate.
Either way the customization runtime resolves the model identically and checks
IsModelInCdimage at apply time (not at registration — a streamed model may not
be indexed yet).
outfit — streamed clothing on a freemode ped
A pack with kind = 'outfit' does not swap the ped model. Each item dresses a
freemode base ped (mp_m_freemode_01 / mp_f_freemode_01) with add-on clothing
the pack streams — component and prop variations in the pack's own collection. Why
this kind exists: a custom ped model changes the silhouette and the damage capsule,
which in a PvP arena is pay-to-win wearing a cosmetic's clothes; freemode components
leave every player on the same rig. base is restricted to the two freemode peds for
exactly that reason (it is also the only rig the add-on clothing pipeline targets).
Shipping the actual clothing assets — the
.ymt/.ydd/.ytdfiles and the ShopPedApparel.metawiring — is its own workflow: Authoring a streamed outfit pack. The reference template iscosmetic_pack_vanguard.
The addressing rule (read this before anything else)
GTA lays every clothing source for a component slot end to end in one flat array: base game, then official DLC, then each add-on pack, in mount order. A global drawable index is an index into that array — so it moves when the gamebuild bumps (new official DLC is inserted ahead of all custom content) or when any other clothing pack is added, removed or reordered. A pack that hardcodes one is not wrong today and right tomorrow: it silently puts a different garment on the player — for a paid catalogue, the worst possible failure.
What is stable is the pair (collection, local index): the collection is the
pack's own variation source (its streamed .ymt), and the local index is the 3-digit
number the pack itself put in its own filenames
(…^jbib_000_u.ydd → drawable 0). Nothing outside the pack can shift it. The
runtime therefore applies pieces with the collection natives
(SetPedCollectionComponentVariation / SetPedCollectionPropIndex), never the
global-index ones — which is also what makes two independent packs on the same slot
trivially collision-free.
So: drawable in a piece is always the index local to the item's collection —
never a number you read out of a trainer or a global clothes list.
pieces[] — the garments
| Field | Type | Required | Notes |
|---|---|---|---|
slot | integer | yes | The variation slot — a component id by default, or a prop id when prop = true. See the vocabulary below. |
prop | boolean | no | true = a prop slot (hat, glasses, watch) rather than a component. |
drawable | integer | yes | Drawable index local to the item's collection — the NNN in the pack's own filenames. Never a global index. |
texture | integer | no (default 0) | Texture variant of that drawable — the a–z letter in the .ytd filename (a = 0, b = 1, …). |
Component slots (the clothing-bearing vocabulary): 0 head, 1 berd (mask),
2 hair, 3 uppr (arms/sleeves), 4 lowr (legs), 5 hand (bag), 6 feet,
7 teef, 8 accs, 9 task (armour), 10 decl, 11 jbib (top). Prop slots
(prop = true): 0 p_head, 1 p_eyes, 2 p_ears, 6 p_lwrist, 7 p_rwrist.
-- one look, one gender (from the cosmetic_pack_vanguard template)
{ id = 'vanguard_strike_m', label = 'Strike', rarity = 'legend',
base = 'mp_m_freemode_01',
pieces = {
{ slot = 11, drawable = 0 }, -- jbib: plate-carrier top
{ slot = 3, drawable = 0 }, -- uppr: its matching arms/sleeves
{ slot = 4, drawable = 0 }, -- lowr: combat trousers
{ slot = 6, drawable = 0 }, -- feet: assault boots
{ slot = 0, drawable = 0, prop = true }, -- p_head: strike helmet
} },Validation rules you will hit
Errors refuse the pack; warnings are advisory:
basemissing, or anything other than the two freemode peds → error (the pipeline cannot dress another ped, and rig parity is the point of the kind).catcontradictingbase→ error (a mislabelled chip sends the player to a garment that cannot exist; omitcat, it is filled in frombase).- No resolvable collection — no pack-level
dlcNameand no itemcollection→ error. piecesmissing/empty, or adrawable/texturethat isn't a non-negative integer → error.dlcNamecontaining anything but lowercase letters/digits/underscore → warning (the streamed.ymtfilename is all-lowercase, so the derived collection name would never be mounted).- A
slotunused on freemode peds (e.g. prop slots 3–5) → warning (the piece will do nothing). drawable >= 128→ warning: that local index means the collection holds 128+ drawables in one slot — the documented safe ceiling per slot per.ymt(255 is the hard crash line). Split the catalogue across a second pack/collection.- A top (
11/jbib) without its matching arms (3/uppr) → warning: a jbib mesh carries no arms of its own, so the previous outfit's sleeves stay on. - Outfit fields (
base/collection/pieces) on a pack whosekindis not'outfit'→ warning (they would be silently ignored — almost always akindthat was never set).
How failures present at runtime
Asset existence is checked at apply time, not at registration (a streamed collection may not be mounted yet):
- Collection not mounted (pack resource not started, its
stream/never downloaded, or the server's Element Club subscription lapsed) → the equip is refused and the log names the collection that was wanted and every collection that IS mounted — the in-game/collectionscommand prints the same list. - Invalid piece (a local index or texture that doesn't exist in the collection) → the piece is skipped with a warning; the rest of the outfit still applies and the skipped slot shows the default garment.
weaponTex — weapon wraps
A pack with kind = 'weaponTex' ships weapon wraps instead of appearances. A wrap
is a global cosmetic — it re-tints every weapon the player carries (the loadout
is arena-level; the look is the player's), Fortnite-style. The item's id is the
wrap id (not a model); its tint is a built-in GTA weapon tint index, and swatch
colours the card (a tint has no render).
definePack{
id = 'liveries', kind = 'weaponTex', status = 'owned', access = 'player',
present = { title = 'Liveries', sub = 'Included', rgb = '120 200 160' },
skins = {
{ id = 'wrap_army', label = 'Army', tint = 4, swatch = '106 122 90' },
{ id = 'wrap_gold', label = 'Gold', tint = 2, swatch = '212 175 55', rarity = 'vip' },
},
}The engine applies the player's equipped wrap at match spawn (SetPedWeaponTintIndex).
See the OSS example packs liveries (free) + precious (VIP) in cosmetic_pack_base,
and the Cosmetics SDK. A streamed weapon texture
(a .ytd swap, not a built-in tint) instead carries an explicit image and no tint.
Thumbnails
A card shows a thumbnail. Rather than set a URL per item, the runtime derives one
from a single admin convar rfx_assets_url (the public assets base — see
server.cfg), under a per-kind subfolder keyed by the item id:
<base>/peds/<id>.webp for a ped skin, <base>/outfits/<id>.webp for an outfit (an
outfit id is not a ped, so its render is an uploaded studio shot, not a stock ped
portrait), <base>/weapons/<id>.webp for a streamed weapon texture. Set an item's
image to override, or leave it and ship the asset at the derived path. A built-in
wrap (carries a tint) uses its swatch, not an image.
See also
- registerPack — how to register the descriptor.
- Authoring a streamed outfit pack — the asset + manifest workflow behind
kind='outfit'. - Presentation block — the shared
presentshowcase fields. - Mode descriptor — the gameplay sibling.
- Cosmetics SDK overview