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
    bash
    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.lua — put 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 hair → hairbeard. npcBeard, stubble inherit from beard → hairbeard. 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-left … bottom-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.VorpLegacyPersist—Deprecated 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