RivalityFX Docsrivalityfx.com ↗

Cosmetics SDK — building cosmetic packs

The cosmetic side of the RivalityFX SDK. A cosmetic pack is a self-contained resource that adds items to the customization screen by registering a plain-data descriptor with the SDK, in one of three kinds:

  • skins (kind='skin') — whole ped models;
  • outfits (kind='outfit') — streamed add-on clothing dressed onto a freemode base ped;
  • weapon wraps (kind='weaponTex') — a global weapon tint.

Skins and outfits share the appearance axis — one equip slot, one player pref, one "Appearance" catalogue; a player wears a ped-model skin or an outfit, never both. Wraps are a second axis with their own pref. It is the exact mirror of the mode SDK: the same registry pattern, a second domain.

One platform SDK, two contracts: exports.sdk:registerMode (gameplay) and exports.sdk:registerPack (cosmetics). Each is versioned independently.

The model

  • The sdk resource owns the contract, the pack registries, and the definePack builder. Registration runs as a shared script, so the descriptor lands in the SDK's client registry (render + apply) and its server registry (the equip-gating authority).
  • customization is a consumer: it mirrors the client registry to render the page and apply cosmetics. Preferences are client KVP (no database); equipping is server-gated against ownership (entitlements).
  • A pack is its own resource that depends on sdk (the contract) and customization (the runtime), and registers via exports.sdk:registerPack.

The base packs ship as an OSS example (cosmetic_pack_base, base-game models, MIT). Two paid-pack templates (also MIT) ship their own streamed assets under stream/ and register through the same public SDK — no first-party shortcut: cosmetic_pack_neon (streamed ped models) and cosmetic_pack_vanguard (streamed add-on clothing / outfits). That is exactly what lets anyone add packs without forking the core.

Build a pack in 3 steps

  1. Scaffold a resource that depends on the SDK + customization:

    -- fxmanifest.lua
    dependencies { 'sdk', 'customization' }
    shared_scripts {
        '@sdk/shared/kit.lua',        -- SDK helpers the contract uses (load first)
        '@sdk/shared/cosmetics.lua',  -- definePack + LuaCATS types
        'pack.lua',                   -- SHARED: registers on the client AND the server
    }
  2. Author the pack and register it:

    -- pack.lua
    local PACK = definePack{
        id = 'mypack', apiVersion = 2, kind = 'skin',
        status = 'owned', access = 'player',
        present = { title = 'My Pack', sub = 'Included', rgb = '34 211 230' },
        skins = {
            { id = 'a_m_m_skater_01', label = 'Skater', rarity = 'common' },
        },
    }
    -- Register now AND re-announce on a live `restart sdk`:
    Sdk.autoRegister(function() exports.sdk:registerPack(PACK) end)
  3. Ship assets (streamed packs only): drop the model files in stream/, named to match each skin id. Base-game ids stream nothing. (Outfit packs ship clothing, not models — their stream/ layout and manifest wiring differ; see below.)

Outfits — streamed clothing

A kind='outfit' pack does not swap the ped model: each item dresses a freemode base ped with add-on clothing the pack streams, addressed by collection + local drawable index so it can never collide with other packs or shift on a gamebuild bump. It is the appearance kind built for a PvP arena — every player stays on the same rig (same hitbox), only the garments change.

Outfit packs have real asset-pipeline requirements — a cloth tool, ShopPedApparel .meta wiring in the manifest, an Element Club subscription on the server — so they get their own guide: Authoring a streamed outfit pack. The descriptor fields are in the pack descriptor; the template is cosmetic_pack_vanguard. Verify a mounted collection in-game with /collections, and test equips with /skin <id> (the appearance axis is one command — skin or outfit id).

Weapon wraps

Beyond appearances, a pack can ship weapon wraps (kind='weaponTex') — a global weapon cosmetic that re-tints every weapon the player carries (the loadout stays arena-level; the look is per-player), Fortnite-style. A wrap is plain data too: a tint (a built-in GTA weapon tint index) + a swatch (the card colour). The OSS example packs liveries (free) and precious (VIP) ship in cosmetic_pack_base.

definePack{
    id = 'liveries', kind = 'weaponTex', status = 'owned',
    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' },
    },
}

customization persists the equipped wrap as a second player pref (rfx:pref:weaponwrap); the engine applies it at match spawn (SetPedWeaponTintIndex, after the loadout is given). Ownership/gating reuse the same kind-agnostic server equip-gate as appearances, so a wrap pack is free / VIP / sku exactly like a skin pack. Test with /wrap <id> · /wraps. Fields: see the pack descriptor.

Thumbnails

Cosmetic cards show a thumbnail derived from a single admin convar rfx_assets_url (the public assets base — see server.cfg): <base>/peds/<id>.webp for a ped skin, <base>/outfits/<id>.webp for an outfit, <base>/weapons/<id>.webp for a streamed weapon texture. Set an item's image to override; a built-in wrap uses its swatch instead.

Reference