RivalityFX Docsrivalityfx.com ↗

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's id is 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's id is an arbitrary equip id (not a model); the model is its base.
  • 'weaponTex' — weapon wraps; the id is a wrap name carrying a tint.

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 targeted

validatePack 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

FieldTypeRequiredDefaultNotes
idstringyesUnique pack id.
apiVersionintegernoCosmetic contract version you target. Pin it.
kindstringno'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.
dlcNamestringoutfitThe 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.
statusstringno'owned''owned' (everyone has it) | 'locked' (gated — unlocked by an active VIP subscription, or bought outright via sku).
accessstringno'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).
unlockstringnoUnlock CTA text (locked packs).
skustringnoTebex 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.
presenttableadvisedThe pack's card identity + showcasetitle / sub / rgb / desc (+ optional hero / info / buy), the SAME shared block a mode uses. present.title is the tab name. See Presentation block.
skinsarrayyesThe 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.

FieldTypeRequiredNotes
idstringyesThe equipped id. kind='skin': the GTA model name. kind='outfit': a free-form id. kind='weaponTex': a wrap name.
basestringoutfitThe freemode ped this outfit dresses: 'mp_m_freemode_01' or 'mp_f_freemode_01' — nothing else (hard error). See below.
collectionstringoutfitThe 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.
piecesarrayoutfitNon-empty array of { slot, drawable, texture?, prop? } — the garments. See below.
labelstringadvisedCard label (defaults to the id).
raritystringno'common' | 'rare' | 'vip' | 'legend' (tick colour only).
catstringnoGender 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.
lockedbooleannoGrey ONE item inside an OWNED pack (e.g. a VIP piece).
lockstringnoShort tag shown on a locked item (e.g. 'VIP').
imagestringnoExplicit 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.
tintintegerweaponTexGTA weapon tint index applied to every weapon the player holds — the built-in wrap payload. See below.
swatchstringweaponTex"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 packid is a base-game ped (a_m_m_skater_01, s_m_y_swat_01, …), already on every client; the pack streams nothing. See cosmetic_pack_base.
  • Proprietary packid is a model the pack streams from its stream/ folder, named to match the id. See the cosmetic_pack_neon template.

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/.ytd files and the ShopPedApparel .meta wiring — is its own workflow: Authoring a streamed outfit pack. The reference template is cosmetic_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

FieldTypeRequiredNotes
slotintegeryesThe variation slot — a component id by default, or a prop id when prop = true. See the vocabulary below.
propbooleannotrue = a prop slot (hat, glasses, watch) rather than a component.
drawableintegeryesDrawable index local to the item's collection — the NNN in the pack's own filenames. Never a global index.
textureintegerno (default 0)Texture variant of that drawable — the az 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:

  • base missing, or anything other than the two freemode peds → error (the pipeline cannot dress another ped, and rig parity is the point of the kind).
  • cat contradicting baseerror (a mislabelled chip sends the player to a garment that cannot exist; omit cat, it is filled in from base).
  • No resolvable collection — no pack-level dlcName and no item collectionerror.
  • pieces missing/empty, or a drawable/texture that isn't a non-negative integer → error.
  • dlcName containing anything but lowercase letters/digits/underscore → warning (the streamed .ymt filename is all-lowercase, so the derived collection name would never be mounted).
  • A slot unused on freemode peds (e.g. prop slots 3–5) → warning (the piece will do nothing).
  • drawable >= 128warning: 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 whose kind is not 'outfit'warning (they would be silently ignored — almost always a kind that 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 /collections command 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