Skip to content

🎭 Character Suite

Documentation relating to the spooni_charcreator & spooni_charselector.

1. Installation

spooni_charcreator works with VORP, RSG, RedEM:RP and RPX.

To install spooni_charcreator:

  • Download the resource
  • Ensure that all requirements are installed
  • Drag and drop the resources into your resources folder
    • spooni_charcreator
    • spooni_charselector — optional, remove if you don't want the selection scene
  • Add these ensures in your server.cfg
    ensure jo_libs
    ensure spooni_libs
    
    ensure spooni_charcreator
    ensure spooni_charselector      # optional — remove if you don't want the selection scene

    ⚠️ Order is critical. Wrong order = character creation silently breaks. Full load order:

    1. Framework Core (vorp_core / rsg-core / redem_roleplay / rpx-core)
    2. Framework Character (vorp_character / rsg-multicharacter / ...)
    3. Framework Inventory (vorp_inventory / rsg-inventory / ...)
    4. jo_libs
    5. spooni_libs
    6. spooni_charcreator
    7. spooni_charselector (optional)

    Why does order matter? jo_libs must start after Core + Inventory for the framework bridge. spooni_libs builds on jo_libs. Creator needs all three. Selector needs Creator for look previews.

  • Now you can configure the script as you like
    • config.lua for both spooni_charcreator and spooni_charselector — the shipped defaults
    • config_user.luaput your own settings here (see below)
  • At the end, restart the server

No SQL needed. Database tables are created automatically on first start.

Update-safe configuration (config_user.lua)

Both resources ship a config_user.lua. Everything you set there is merged over config.lua, so you never lose your settings on an update — replace every file except config_user.lua and you are done.

lua
-- spooni_charcreator/config_user.lua — same keys as config.lua, without the `Config.` prefix
ConfigUser = {
    Locale = 'de',
    Debug  = false,

    -- Only the keys you name are overridden, the rest of EditorKeys stays default
    EditorKeys = {
        zoomIn  = { rawKey = 107 },   -- numpad +
        zoomOut = { rawKey = 109 },   -- numpad -
    },
}
  • Tables are merged key by key — options added in a future update keep their new default.
  • Lists/arrays are replaced as a whole — if you override Config.Scenes, give the full list.
  • On start the console prints which overrides were applied, so you can verify the file is read.

Framework-Specific Guards

After copying the folders, add guards to prevent your native creator/selector from running alongside Spooni. Then run /spooni_check in the server console — every line must show PASS.

VORP

Creator — copy these 4 patch files from spooni_charcreator/docs/patches/vorp_character/:

Copy this fileOver this file
client/createCharacter.luavorp_character/client/createCharacter.lua
client/commands.luavorp_character/client/commands.lua
client/client.luavorp_character/client/client.lua
server/server.luavorp_character/server/server.lua

Modified vorp_character yourself? Follow the manual merge in docs/INSTALL-VORP.md Appendix B.

Selector — add ONE line in vorp_character/client/client.lua, handler vorpcharacter:selectCharacter (~line 90):

lua
RegisterNetEvent("vorpcharacter:selectCharacter", function(myCharacters, mc, rand)
    if GetResourceState('spooni_charselector') == 'started' then return end -- NEW
    -- ... rest of the handler ...
end)
RSG

Creator — copy these 2 patch files from spooni_charcreator/docs/patches/:

Copy this fileOver this file
rsg-multicharacter/client/main.luarsg-multicharacter/client/main.lua
rsg-appearance/client/creator.luarsg-appearance/client/creator.lua

Files in resource root instead of client/? Copy there instead.

Selector — add ONE line in rsg-multicharacter/client/main.lua, handler rsg-multicharacter:client:chooseChar (~line 196):

lua
RegisterNetEvent('rsg-multicharacter:client:chooseChar', function()
    if GetResourceState('spooni_charselector') == 'started' then return end -- NEW
    -- ... rest of the handler ...
end)

Do not swap the guard resource names. Selector guard checks spooni_charselector, creator guard checks spooni_charcreator. Swapped = naked characters.

RedEM:RP

Single guard in redemrp_identity/client/cl_main.lua, handler redemrp_identity:openSelectionMenu (~line 72):

lua
if GetResourceState('spooni_charselector') == 'started' then return end

Always run Creator + Selector together on RedEM:RP.

RPX

Single guard in rpx-multicharacter/client.lua, handler CLIENT:MultiCharacter:LoadCharacters (~line 177):

lua
if GetResourceState('spooni_charselector') == 'started' then return end

Always run Creator + Selector together on RPX. RPX has no SecondChance item support — use the /secondchance command or exports.spooni_charcreator:OpenSecondChance().

Custom Framework

If your own code controls the character flow, trigger directly instead of patching:

lua
-- Creator
TriggerClientEvent('spooni_charcreator:start', src)

-- Selector
TriggerClientEvent('spooni_charselector:open', src, chars, maxSlots, rand)

Wire your core in framework.lua — see the Custom Framework Integration section under For developers.

If you have any problems, you can always open a ticket in the Spooni Discord.

2. Usage

A complete character creation and selection system for RedM. The Creator replaces your framework's native editor with live camera views, item-gated sections, NPC hairstyles & beards, body size tuning, skirts for female characters, and vanilla store takeover. The Selector adds an atmospheric in-world campfire scene where characters stand in their real look with ambient music and a spawn location picker.

3. For developers

Everything works through exports & hooks — no source access needed. Current version 1.8.42.

Creator Configuration (spooni_charcreator/config.lua)

Framework & Language

lua
Config.Framework = 'auto'   -- 'auto', 'vorp', 'rsg', 'rpx', 'redemrp'
Config.Locale    = 'auto'   -- 'auto' (follows spooni_libs) or 'de', 'en', 'fr', ...
Config.Debug     = false    -- true for testing only

Editor Sections (Config.Categories)

Each section toggles individually. true = visible, false = hidden.

lua
Config.Categories = {
    -- Top-level
    skintone    = true,    -- Skin tone
    body        = true,    -- Body build + size
    face        = true,    -- Face morphs (8 groups)
    eyesteeth   = true,    -- Eyes & teeth
    hairbeard   = true,    -- Hair & beard (master switch)
    skindetails = true,    -- Skin details (freckles, pores)
    scars       = true,    -- Scars
    makeup      = true,    -- Makeup
    clothing    = true,    -- Clothing
    identity    = true,    -- Name/age (creation only)

    -- Hair & Beard sub-sections
    hair        = true,    -- Hair styles
    beard       = true,    -- Beard styles
    hairColor   = true,    -- Hair color swatches
    npcHair     = true,    -- 🏆 NPC hairstyles (drawable-based)
    bonnet      = true,    -- Hat/bonnet hair (female)
    npcBeard    = true,    -- 🏆 NPC full beards (drawable-based)
    stubble     = true,    -- Beard stubble
    eyebrows    = true,    -- Eyebrows

    -- Body sub-sections
    bodyBuild   = true,    -- Body build presets
    bodyMorphs  = true,    -- Body shape sliders
    bodySize    = true,    -- Character height

    -- Eyes & Teeth sub-sections
    eyeRedness  = true,    -- Bloodshot eyes
    eyeColor    = true,    -- Eye color
    teeth       = true,    -- Teeth model

    -- Free color sliders
    dye         = true,    -- Free color (beyond preset swatches)
}

Inheritance: hairColor, npcHair, bonnet inherit from hairhairbeard. npcBeard, stubble inherit from beardhairbeard. eyebrows inherits from both hair and makeup (OR — visible if either parent is on).

NPC Hair & Beards

The editor includes NPC hairstyles (npcHair) and NPC full beards (npcBeard) - caution it can clip with certain hats/hair!

lua
-- Enable NPC hair/beards (both on by default)
Config.Categories = {
    npcHair  = true,    -- NPC hairstyles visible in Hair & Beard menu
    npcBeard = true,    -- NPC full beards visible in Hair & Beard menu
}

NPC hair lives under Hair & Beard → inherits from hair. NPC beards inherit from beard. Turn either parent off and its NPC variant hides too.

Item-Gated Sections (Config.UseItems)

Items that open the editor when used. Perfect for barber kits, makeup kits, or paid re-customization.

lua
Config.UseItems = {
    -- Full editor, one-time use
    { item = 'secondchance', consumable = true,  sections = nil },

    -- Barber kit: hair & beard only, reusable
    { item = 'shaving_kit', consumable = false,
      sections = { 'hair', 'hairColor', 'npcHair', 'bonnet',
                   'beard', 'stubble', 'npcBeard', 'dye' } },

    -- Makeup kit: makeup only, reusable
    { item = 'makeup_kit',  consumable = false,
      sections = { 'makeup', 'dye' } },
}
FieldDescription
itemItem name — must exist in your database
consumabletrue = used up on use, false = stays in inventory
sectionsnil = full editor, list = only those sections

After adding a new item: Full server restart (inventories cache items at boot).

SecondChance Command

lua
Config.SecondChanceCommand = 'secondchance'   -- players type /secondchance turn this false because this is not gated! Use item for players!

Name Rules

lua
Config.RequireName = true   -- first + last name required
Config.NameBlacklist = {
    'John Marston', 'Arthur Morgan', 'Dutch van der Linde',
    'Micah Bell', 'John Doe', 'Max Mustermann',
}

Editor Scene

lua
Config.Scene = {
    mode    = 'simple',  -- 'simple' | 'room' | 'cinematic'
    coords  = vector3(2546.3027, -1307.7715, 48.5166),
    heading = 357.8737,
    hour    = 20,
}
  • 'simple' — direct entry with camera movement
  • 'room' — Sheriff office interior
  • 'cinematic' — Full cinematic intro with gender selection
lua
Config.SimpleGenderStep = true

Guides new players through gender → continue → identity → appearance instead of dropping them into the identity step right away. With this on, the gender switch is removed from the main menu (it would be the same choice twice); set it to false and the switch reappears there. Irrelevant in 'cinematic' mode — that one picks gender at the two intro peds.

Editor Keys

Rebind rotate / camera up-down / zoom. Two input types per action: a game control (can be shown as an on-screen prompt) or a rawKey (any keyboard key, no prompt).

lua
Config.EditorKeys = {
    rotateLeft  = { control = 'INPUT_FRONTEND_LS' },            -- Z
    rotateRight = { control = 'INPUT_CREATOR_RS' },             -- C
    camUp       = { control = 'INPUT_SHOP_SELL' },              -- R
    camDown     = { control = 'INPUT_GAME_MENU_EXTRA_OPTION' }, -- F
    zoomIn      = { control = 'INPUT_SHOP_BOUNTY' },            -- B
    zoomOut     = { control = 'INPUT_PUSH_TO_TALK' },           -- N
}
lua
zoomIn  = { rawKey = 107 },                               -- numpad +
camUp   = { control = 'INPUT_CREATOR_LT', rawKey = 33 },  -- prompt + page up
camDown = 'INPUT_CREATOR_RT',                             -- short form

Common raw key codes: arrows 37/38/39/40, page up/down 33/34, letters A 65 … Z 90, numbers 0 48 … 9 57, numpad 0 96 … 9 105 (+ 107, - 109), space 32, F1–F12 112…123.

The mouse wheel is deliberately not used for zoom — the menu scrolls its lists with it.

Body Sizes

lua
Config.Sizes = {
    presets = {
        { labelKey = 'size_small',  value = 0.95 },
        { labelKey = 'size_normal', value = 1.00 },
        { labelKey = 'size_large',  value = 1.05 },
    },
    allowCustom    = false,
    recommendedMin = 0.92,
    recommendedMax = 1.08,
    hardMin = 0.70,  hardMax = 1.40,
}

Camera Views

Fine-tune camera per editor section:

lua
Config.CameraViews = {
    head = { offset = vector3(0.0957, 1.6212, 0.5850), fov = 20.01, focus = 1.4 },
    body = { offset = vector3(0.1057, 2.9811, 0.2037), fov = 35.0,  focus = 1.0 },
    hair = { offset = vector3(0.0957, 1.6311, 0.5550), fov = 25.0,  focus = 1.6 },
    legs = { offset = vector3(0.1000, 2.2000, -0.6200), fov = 30.0, focus = 1.2 },
}
Config.DefaultCameraView = 'body'

Starter Outfit

lua
Config.StarterOutfit = {
    enabled  = true,
    item     = 'starter_outfit',
    quantity = 1,
}

New characters get a re-equip item for their created outfit - good so they don't lose their first outfit. You can turn this off if you don't want to use this.

Intro Animation

lua
Config.Intro = {
    enabled  = true,
    title    = 'Start your journey', -- name it whatever you want
    duration = 5500,
    distMult = 4.0,
    font     = 22,
}

Murphy Compatibility

Only if you also run murphy_clothing / murphy_barber:

lua
Config.Murphy = {
    clothing = false,   -- true if murphy_clothing runs
    barber   = false,   -- true if murphy_barber runs
    clothingResource = 'murphy_clothing',
    barberResource   = 'murphy_barber',
}

Requires murphy_clothing 3.44.0+ / murphy_barber 1.21.0+

Jo_clothingstore & hairdresser

Compatible out of the box just turn off the VanillaTakeover for shops.

Optional: send new characters shopping after they spawn

lua
Config.OpenClothingStoreAfterCreate = {
    enabled = false,          -- true = open jo_clothingstore for new characters
    delayMs = 1500,           -- grace period after the spawn
    options = {               -- passed straight to jo_clothingstore's openStore export
        useOutfitMenu         = true,
        useNPCClothesMenu     = true,
        useClassicClothesMenu = true,
    },
}

⚠️ When you enable this, set Config.openStoreNewCharacter = false in the jo_clothingstore config. We trigger the store ourselves — if their auto-open stays on, it fires as well and the store opens twice. The creator checks their config on start and prints a warning if it is still true.

Why let us open it: we call jo's export with needInstance = true, so the player changes clothes alone instead of in front of everyone at the spawn point — and we wait for the real end of the entry flow (spawn carousel included) instead of firing during creation.

Other creator options

OptionDefaultWhat it does
Config.MenuSubtitle'Charcreator'Free text in the editor's menu header — your server name or a tagline
Config.MenuItemFont'hapna'Font for menu button labels. 'crock' = the game's western-style font
Config.NotifyPlacement'middle-right'Where notifications appear (top-leftbottom-right)
Config.FemaleUnderweartrueKeeps the female body covered. Set to false only if you stream a full female body replacement
Config.SkinSwapMask'blur'How the short flicker on a skin-tone change is hidden: 'blur', 'alpha' or 'off'
Config.DyePalettestableColour palettes for free dyeing (turned off globally by Config.Categories.dye)
Config.CinematictableSettings for Config.Scene.mode = 'cinematic' only
Config.CleanupOrphanedExtrasfalseOn start, delete stored appearance data of characters that no longer exist in your framework database
Config.VorpLegacyPersistDeprecated and ignored. Since 1.2.0 the VORP save path is always one server-authoritative commit with read-back verification

Discord webhooks (spooni_charcreator/server/config_webhooks.lua)

Optional Discord messages for your server log channel. They live in their own file because it is the one part of the resource you enter your own data into:

lua
Webhooks.charCreated = ''   -- posted when a player finishes a new character
Webhooks.itemUsed    = ''   -- posted when a player uses one of this resource's items

Leave a URL empty to keep that message off — that is the default. The file is server-side only, so the URLs are never sent to players, and it ships readable so you can edit it.

To get a URL: Discord → Server Settings → Integrations → Webhooks → New Webhook → pick a channel → Copy Webhook URL.

Webhooks are purely informational. A missing, wrong or expired URL never blocks character creation or item use.

Updating from 1.8.40 or older: the URLs used to sit in server/sv_webhooks.lua, which is encrypted and could not be edited. They now live in server/config_webhooks.lua. Enter them there once.

Selector Configuration (spooni_charselector/config.lua)

Framework & Slots

lua
Config.Framework  = 'auto'
Config.Locale     = 'auto'
Config.MaxSlots    = 6         -- max slots shown (must match scene positions) !!!!!!!!!!
Config.AllowDelete = true      -- allow character deletion
Config.DefaultScene = 1        -- starting scene (index in Config.Scenes)
Config.RandomScene = false     -- random scene each time
Config.SkipSelectorUnderSlots = 2 -- If someone has 1 character he skips the scene to faster load in.
Config.FallbackSpawn = vector4(-275.5, 803.2, 118.4, 0.0)

RSG only:

lua
Config.RSGCharListTimeout = 15000

How long to wait for the RSG character-load callback chain before showing an empty list so the screen stays usable. This is not a cut-off — the characters keep loading in the background and appear as soon as they arrive, so nothing is ever lost by it being low. A single stuck character gets its own deadline (a third of this value) and never blocks the rest of the list. Only raise it for a genuinely slow database — a higher value means the player stares at a loading screen longer, and one bad character blocks the list longer.

Screen Corners

Where the control hints and the footer row (gear + logout button) sit. Any of the four corners; put both in the same corner and they stack automatically.

lua
Config.ControlsCorner = 'top-right'     -- 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
Config.FooterCorner   = 'bottom-right'

Other selector options

OptionDefaultWhat it does
Config.SkipSelectorWhenEmptyfalsefalse = a brand new player sees the selector with an empty ghost slot first. true = jump straight into the creator on first join
Config.UiFont'RDRLino'Font for the whole selector UI. Custom fonts: add an @font-face block at the top of html/style.css and use that name here
Config.PreferFavoriteScenetrueIf the player favorited scenes (gear menu), open on a random one of those instead of Config.DefaultScene
Config.FocustableCamera move/zoom when cycling between characters — purely visual
Config.HovertableWhat the selected character does (look at camera, idle animation)
Config.SceneLoadtableHow thoroughly the scene is loaded before it fades in, so characters never appear floating or half-loaded
Config.PreviewBaseBodyCovertrueRSG: injects a gender-matching base garment for uncovered body parts, because rsg-appearance resolves its fallback against the local player's gender and would otherwise clip. Turn off only if you stream a full-body mesh replacement

Discord webhook (spooni_charselector/server/config_webhooks.lua)

Optional Discord message when a player creates a new character. Its own file, because it is the one part you enter your own data into:

lua
Webhook.onCreate = true          -- post when a character is created
Webhook.url      = ''            -- Discord webhook URL, empty = off
Webhook.username = 'Char Selector'
Webhook.avatar   = ''            -- image URL for the sender avatar, empty = none
Webhook.color    = 3447003       -- embed colour as a decimal number

Server-side only, so the URL is never sent to players, and it ships readable so you can edit it. Empty URL = off, which is the default.

To get a URL: Discord → Server Settings → Integrations → Webhooks → New Webhook → pick a channel → Copy Webhook URL.

Updating from 1.4.12 or older: the settings used to sit in server/sv_webhooks.lua, which is encrypted and could not be edited. Enter them in server/config_webhooks.lua once.

Branding

lua
Config.Brand = {
    name     = 'Your Server', -- just leave like '' this for no Branding
    subtitle = 'Year 1899',
    font     = "'RDRLino', serif", -- put any font into html folder and use it
}

Ghost Slots (empty places)

lua
Config.Ghost = {
    model    = 'mp_male',
    alpha    = 100,
    scenario = 'MP_LOBBY_SCENARIO_08',
    outfit   = {},
}

Lighting

lua
Config.FillLight = {
    enabled = true, range = 4.0, intensity = 5.0,
    color = { r = 255, g = 226, b = 190 },
}

Config.Spotlight = {
    enabled = true, range = 6.5, intensity = 22.0,
    color = { r = 255, g = 220, b = 170 },
    dimOthers = true,
}

Music

lua
Config.DefaultVolume     = 0.10
Config.MaxVolume         = 0.45
Config.AutoPlayFirstSong = true

Config.Songs = {
    { label = 'Blacktop Ember',     file = 'sounds/blacktop_ember.mp3' },
    { label = 'Dust On The Fence',  file = 'sounds/dust_on_the_fence.mp3' },
    { label = 'Welcome Theme',      file = 'sounds/welcome_theme.mp3' },
}

Adding your own music: Drop .mp3/.ogg into html/sounds/, add filenames to html/sounds/sounds.json, then restart spooni_charselector.

Idle Animations (Gear Menu)

lua
Config.SelectableScenarios = {
    { key = 'scenario_stand_calm',      name = 'MP_LOBBY_STANDING_B' },
    { key = 'scenario_fire_sit',        name = 'world_human_fire_sit' },
    { key = 'scenario_drink',           name = 'WORLD_HUMAN_DRINK' },
    { key = 'scenario_smoke_wall',      name = 'WORLD_HUMAN_LEAN_BACK_WALL_SMOKING' },
    { key = 'scenario_lean_railing',    name = 'WORLD_HUMAN_LEAN_BACK_RAILING' },
    { key = 'scenario_write_notebook',  name = 'WORLD_HUMAN_WRITE_NOTEBOOK' },
    { key = 'scenario_clipboard',       name = 'WORLD_HUMAN_CLIPBOARD' },
    { key = 'scenario_coffee_drink',    name = 'WORLD_HUMAN_COFFEE_DRINK' },
    { key = 'scenario_lean_wall_drink', name = 'WORLD_HUMAN_LEAN_WALL_DRINKING' },
}

Scenes

Two scenes ship pre-installed — Theater (Saint Denis) and Riverside. Add your own in Config.Scenes:

lua
{
    name = 'My Scene',
    env = {
        weather     = { type = 'sunny', transition = 0.0, snow = false },
        time        = { hour = 12, minute = 0 },
        playerSpawn = vector3(0.0, 0.0, 100.0),   -- must be SOLID GROUND
        campfire    = { enabled = true, model = 'p_campfirecombined01x',
                        coords = vector3(0.0, 0.0, 100.0), heading = 0.0 },
    },
    masterCam = { x = 0.0, y = 10.0, z = 102.0, fov = 53.0 },
    positions = {
        { spawn = vector4(2.0, 1.0, 100.0, 180.0), scenario = {
            mp_male = { 'MP_LOBBY_STANDING_B' },
            mp_female = { 'MP_LOBBY_STANDING_B' } } },
        -- ... one position per slot (matches Config.MaxSlots) ...
    },
}

Two rules for custom scenes — break either and the peds float, sink or stay untextured:

  1. env.playerSpawn must be at the scene, on solid ground. That is where the player is parked while the scene runs, and the map is loaded around it. Copying a scene block and forgetting to move its playerSpawn is the single most common mistake — the peds then stand in terrain that was never streamed in.
  2. Take the z of every position from standing on that exact spot, not from a flying camera. A z more than 1.5 m off the real ground is treated as a configuration error and used as-is, so the ped floats or sinks.

Both are checked at runtime — the console names the scene, the coordinates and how far off the z is, so you can fix it without guessing. Scenes on special/add-on maps (Guarma) also need Config.SceneLoad.cleanLoad = true.

Spawn Module (Config.Spawn)

Live-camera spawn location picker for new characters:

lua
Config.Spawn = {
    enabled = true,
    locations = {
        { title = "Saint Denis", subtitle = "City of lights and lies",
          coords = vector3(2731.63, -1442.33, 46.26), heading = 224.39,
          cam = vector4(3073.86, -1326.31, 177.92, 85.33),
          job = false },   -- false = everyone

        { title = "Valentine", subtitle = "A rowdy cattle town",
          coords = vector3(-166.19, 633.43, 114.08), heading = 307.67,
          cam = vector4(-420.58, 828.37, 156.65, 243.09),
          job = false },

        -- Job-gated example:
        { title = "Sheriff's Office", coords = vector3(...), heading = 0.0,
          job = 'lawman' },
    },
}

Job-gated locations: job = false for everyone, job = 'lawman' or job = { 'bartender', 'dancer' } for specific jobs. jobGrade = 2 for minimum grade. Always keep at least one location with job = false.

Custom step before spawn menu:

lua
Config.Spawn.beforeMenu = {
    event     = 'myjobs:openSelector',
    doneEvent = 'myjobs:selectorDone',
    timeoutMs = 120000,
}

Can be used for own UIs to select Jobs and handover to spawnselect that can be jobgated.

RSG conflict: disable rsg-spawn (Config.SelectLocations = false) or set Config.Spawn.enabled = false.

Admin Command

lua
Config.AdminCommand = 'charselect'   -- '' = disabled
Config.AdminGroups  = { 'admin', 'superadmin' }

Usage: /charselect <serverId> in server console.

Vanilla Store Takeover

Replaces the built-in clothing/barber/makeup stores of VORP/RSG with the Spooni editor — scoped to just that section, with server-side price calculation. TURN THIS OFF IF YOU HAVE ANOTHER BARBER/HAIRDRESSER/CLOTHINGSTORE

Configuration

lua
Config.VanillaStoreTakeover = {
    enabled  = true,
    currency = 0,      -- 0 = dollars, 1 = gold, 2 = rol

    prices = {
        clothing = {
            hats = 3.0, masks = 3.0, eyewear = 3.0, neckwear = 3.0, neckties = 3.0,
            shirts_full = 4.0, vests = 4.0, coats = 5.0, coats_closed = 5.0,
            ponchos = 5.0, suspenders = 3.0, armor = 6.0, dresses = 5.0,
            gloves = 3.0, gauntlets = 3.0,
            pants = 4.0, chaps = 4.0, spats = 3.0, boots = 4.0, boot_accessories = 2.0,
            belts = 3.0, belt_buckles = 2.0, satchels = 4.0, accessories = 3.0,
            jewelry_bracelets = 3.0, jewelry_rings_left = 3.0, jewelry_rings_right = 3.0,
            gunbelts = 4.0, gunbelt_accs = 3.0, holsters_left = 3.0, loadouts = 4.0,
        },
        barber = 5.0,   -- flat fee, once per visit if anything changed
        makeup = 3.0,   -- flat fee, same rule
    },
}

How It Works

  • Clothing: Price per changed category — swap only your hat, only hat price charged
  • Barber/Makeup: Flat fee per visit, only if anything changed. Close without changes = free
  • Server-side: Prices computed from the actually-persisted look — clients can't fake the price

Required Guards

StoreVORPRSG
ClothingCopy docs/patches/vorp_character/client/clothingstore.luaCopy docs/patches/rsg-appearance/client/clothes.lua
Barber/HairSame file (vorp handles all shops in clothingstore.lua)Already in creator.lua patch (Framework-Specific Guards)
rsg-barbers (standalone)Copy docs/patches/rsg-barbers/client/barber-guard.lua

Without these guards, VanillaStoreTakeover does nothing.

Developer API & Exports

Full Export Reference

spooni_charcreator — Client

ExportSignatureDescription
OpenAppearanceEditor(opts) -> boolStart a scoped editor session
CommitLook(snapshot) -> boolPersist + apply + close session
AbortEditor()Close session, revert look
GetCurrentLook() -> table|nilDeep copy {skin, clothes}
RandomizeAppearance(scope)Randomize during active session
SetEditorMenuVisible(bool)Show/hide menu
IsEditorBusy() -> boolCheck if editor is free
ReapplyLook()Re-apply saved look
OpenSecondChance()Open SecondChance editor
OpenVanillaStore(shopType, storeData?) -> boolStore takeover: 'clothing'/'hair'/'makeup'/'secondchance'
ApplyLookToPed(ped, charId)Apply saved look to any ped
ApplyDefaultLookToPed(ped, gender)Apply default look — for ghost slots
ApplyMurphyToPed(ped, charId)Apply Murphy overlays to ped
IsMurphyEnabled() -> boolMurphy switch state
GetMurphyConfig() -> table|nilLive Config.Murphy
CleanupMurphyPed(ped)Required after deleting a Murphy ped
CleanupMurphyAll()Free all Murphy handles
registerAction(name, fn, priority?)Register hook action
registerFilter(name, fn, priority?)Register hook filter

spooni_charcreator — Server

ExportSignatureDescription
GetMaxSlots(src) -> number|nilResolved max slots for a player
registerAction(name, fn, priority?)Register hook action
registerFilter(name, fn, priority?)Register hook filter

spooni_charselector — Client

ExportSignatureDescription
RegisterPreSpawnStep(fn) -> boolStep before spawn menu, call done() when finished
HandlesNewCharSpawn() -> boolInternal (RSG): selector owns post-create spawn

Hooks

lua
-- Action
exports.spooni_charcreator:registerAction('spooni:afterSave', function(src, charId)
    print(('Char %s saved by player %s'):format(charId, src))
end, 10)

-- Filter
exports.spooni_charcreator:registerFilter('spooni:sectionAllowed', function(value, token, sessionId)
    if token == 'npcHair' and GetPlayerJob() ~= 'barber' then
        return false   -- NPC hair only for barbers
    end
    return value
end, 10)
HookTypeSideWhen
spooni:beforeEditorOpenActionClientBefore menu build
spooni:afterEditorOpenActionClientAfter successful menu build
spooni:beforeSaveActionClientBefore save (snapshot is read-only)
spooni:editorClosedActionClientOn every editor close
spooni:afterSaveActionServerAfter successful save
spooni:afterCreateActionServerFirst save of a character
spooni:sectionAllowedFilterClientEvery section visibility check
spooni:editorCategoriesFilterClientBefore applying session categories
spooni:maxSlotsFilterServerFinal step of slot resolution

Practical Examples

Barber NPC:

lua
exports.spooni_charcreator:OpenAppearanceEditor({
    id = 'barber', categories = { hairbeard = true },
    title = 'Barber', subtitle = 'Hair & Beard',
})

Tailor NPC (clothing only):

lua
exports.spooni_charcreator:OpenAppearanceEditor({
    id = 'tailor', categories = { clothing = true, dye = true },
    title = 'Tailor', subtitle = 'Pick your outfit',
})

Eyebrow pencil item:

lua
Config.UseItems = {
    { item = 'eyebrow_pencil', consumable = true,
      sections = { 'eyebrows', 'dye' } },
}

Dynamic slot bonus for supporters:

lua
-- server-side
exports.spooni_charcreator:registerFilter('spooni:maxSlots', function(value, src, identifier)
    if IsSupporter(identifier) then value = (value or 1) + 2 end
    return value
end, 10)
Custom Framework Integration

Wire a custom core in framework.lua. Use the existing VORP/RSG/RedEM:RP/RPX branches as a reference.

Creator (framework.lua in spooni_charcreator)

Client-side:

lua
function Framework.CreateCharacter(data, onSelected)
    -- data = { firstname, lastname, age, gender, model }
    -- Create char, call onSelected(charid)
end

function Framework.OnCharacterSelected(cb)  -- cb(isNew)
function Framework.OnRespawn(cb)            -- cb(reason)
function Framework.IsSessionStarted()       -- bool

Server-side:

lua
function Framework.GetLookForChar(charId, cb)    -- cb(skin, clothes)
function Framework.CheckOwnership(src, id, cb)   -- cb(owned: bool)

Selector (framework.lua in spooni_charselector)

Client-side:

lua
function Framework.OnCharactersReady(cb)    -- cb(chars, maxSlots)
function Framework.GetMaxSlots()            -- number
function Framework.PlayCharacter(char)      -- select + spawn
function Framework.RequestDelete(char)      -- server-authoritative delete
function Framework.StartCreator(opts)       -- open creator for empty slot
function Framework.ApplyLookToPed(ped, char) -- preview look on slot ped

Character object shape:

lua
{
    charIdentifier = id,
    firstname = "John", lastname = "Doe",
    skin   = { sex = 'mp_male' },
    coords = { x, y, z, heading },
    isDead = false,
    _raw   = <core-native row>,
    _look  = { kind = 'yourcore', ... }
}
Troubleshooting

General

SymptomCauseFix
Two creators open / duplicate pedsGuard missing or not first lineRe-check guard, restart
Look reverts after relogWrong core detectedSet Config.Framework to your core
"No supported core"Core name/order wrongCore must start BEFORE spooni, named vorp_core/rsg-core/etc.
Item not usableInventory caches at bootFull server restart
/spooni_check shows FAILGuard missingRe-apply the named guard

VORP

SymptomFix
Naked after /rcRe-copy patch from Framework-Specific Guards
attempt to call nil (updateCompTints)Outdated vorp_core — spooni auto-falls-back to DB direct write
A player is stuck on a black loading screen and never reaches character selection, console shows client.lua:322: attempt to compare number with stringA value in that character's skinPlayer row is stored as text instead of a number, which kills vorp's own load routine. It is a deadlock: the player never reaches the editor, so nothing can repair it in-game. Creator 1.8.41+ fixes both ends — the value is repaired while loading, and the save path no longer writes it. Update, re-copy docs/patches/vorp_character/client/client.lua, and affected players can log in again

RSG

SymptomFix
Naked after selectionGuards swapped. Selector guard = spooni_charselector, Creator guard = spooni_charcreator
Clothing store opens vanilla RSGclothes.lua guard missing or VanillaStoreTakeover.enabled = false
A new character is gone after relogrsg-core writes the players row without waiting for the database, so a failed insert is never reported. Creator 1.8.23+ verifies the row after creation and prints a ready-to-run repair INSERT if it is missing
Character list slow / empty at firstLeave Config.RSGCharListTimeout at its default — see Selector Configuration. Raising it does not load anything faster
Appearance jumps back to an older look after /loadskin or a relogplayerskins holds more than one row for that citizenid, and RSG reads the first one it finds — so the oldest keeps winning no matter how often the player saves. Creator 1.8.42+ updates the existing row instead of adding another, and prints a warning naming the affected citizenid. Existing duplicates have to be cleaned up once in the database; afterwards add UNIQUE KEY (citizenid) to playerskins so it cannot happen again

Selector

SymptomFix
Slot peds naked/blankspooni_charcreator not running
Custom scene: peds float, sink or have no texturesenv.playerSpawn is missing or points at another scene — the map is loaded around it, so the peds end up in unstreamed terrain. Selector 1.4.11+ loads around the character positions instead and names the broken scene in the console
One character missing from the carousel (back after a reconnect)Its preview ped model did not stream in fast enough and the slot was skipped. Fixed in selector 1.4.10+ — it retries, keeps the slot selectable either way, and logs the model + character id if it still fails
Character invisible / slot looks empty on RSGrsg-appearance hides the ped while dressing it and only makes it visible again at the very end, so any abort leaves it invisible. Selector 1.4.9+ forces our preview peds visible
Native selection still showsGuard missing or not first line
Two spawn menusRSG: Config.SelectLocations = false in rsg-spawn. RPX: don't start rpx-spawn
No musicFiles must be in html/sounds/ AND listed in sounds.json