🎭 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
- jo_libs (v2.12+)
- spooni_libs (latest)
- Drag and drop the resources into your resources folder
spooni_charcreatorspooni_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:
- Framework Core (
vorp_core/rsg-core/redem_roleplay/rpx-core) - Framework Character (
vorp_character/rsg-multicharacter/ ...) - Framework Inventory (
vorp_inventory/rsg-inventory/ ...) jo_libsspooni_libsspooni_charcreatorspooni_charselector(optional)
Why does order matter?
jo_libsmust start after Core + Inventory for the framework bridge.spooni_libsbuilds onjo_libs. Creator needs all three. Selector needs Creator for look previews. - Framework Core (
- Now you can configure the script as you like
config.luafor bothspooni_charcreatorandspooni_charselector— the shipped defaultsconfig_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.
-- 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 file | Over this file |
|---|---|
client/createCharacter.lua | vorp_character/client/createCharacter.lua |
client/commands.lua | vorp_character/client/commands.lua |
client/client.lua | vorp_character/client/client.lua |
server/server.lua | vorp_character/server/server.lua |
Modified
vorp_characteryourself? Follow the manual merge indocs/INSTALL-VORP.mdAppendix B.
Selector — add ONE line in vorp_character/client/client.lua, handler vorpcharacter:selectCharacter (~line 90):
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 file | Over this file |
|---|---|
rsg-multicharacter/client/main.lua | rsg-multicharacter/client/main.lua |
rsg-appearance/client/creator.lua | rsg-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):
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):
if GetResourceState('spooni_charselector') == 'started' then return endAlways run Creator + Selector together on RedEM:RP.
RPX
Single guard in rpx-multicharacter/client.lua, handler CLIENT:MultiCharacter:LoadCharacters (~line 177):
if GetResourceState('spooni_charselector') == 'started' then return endAlways 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:
-- 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
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 onlyEditor Sections (Config.Categories)
Each section toggles individually. true = visible, false = hidden.
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!
-- 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.
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' } },
}| Field | Description |
|---|---|
item | Item name — must exist in your database |
consumable | true = used up on use, false = stays in inventory |
sections | nil = full editor, list = only those sections |
After adding a new item: Full server restart (inventories cache items at boot).
SecondChance Command
Config.SecondChanceCommand = 'secondchance' -- players type /secondchance turn this false because this is not gated! Use item for players!Name Rules
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
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
Config.SimpleGenderStep = trueGuides 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).
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
}zoomIn = { rawKey = 107 }, -- numpad +
camUp = { control = 'INPUT_CREATOR_LT', rawKey = 33 }, -- prompt + page up
camDown = 'INPUT_CREATOR_RT', -- short formCommon 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
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:
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
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
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:
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
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 = falsein 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 stilltrue.
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
| Option | Default | What 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.FemaleUnderwear | true | Keeps 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.DyePalettes | table | Colour palettes for free dyeing (turned off globally by Config.Categories.dye) |
Config.Cinematic | table | Settings for Config.Scene.mode = 'cinematic' only |
Config.CleanupOrphanedExtras | false | On 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:
Webhooks.charCreated = '' -- posted when a player finishes a new character
Webhooks.itemUsed = '' -- posted when a player uses one of this resource's itemsLeave 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 inserver/config_webhooks.lua. Enter them there once.
Selector Configuration (spooni_charselector/config.lua)
Framework & Slots
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:
Config.RSGCharListTimeout = 15000How 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.
Config.ControlsCorner = 'top-right' -- 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
Config.FooterCorner = 'bottom-right'Other selector options
| Option | Default | What it does |
|---|---|---|
Config.SkipSelectorWhenEmpty | false | false = 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.PreferFavoriteScene | true | If the player favorited scenes (gear menu), open on a random one of those instead of Config.DefaultScene |
Config.Focus | table | Camera move/zoom when cycling between characters — purely visual |
Config.Hover | table | What the selected character does (look at camera, idle animation) |
Config.SceneLoad | table | How thoroughly the scene is loaded before it fades in, so characters never appear floating or half-loaded |
Config.PreviewBaseBodyCover | true | RSG: 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:
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 numberServer-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 inserver/config_webhooks.luaonce.
Branding
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)
Config.Ghost = {
model = 'mp_male',
alpha = 100,
scenario = 'MP_LOBBY_SCENARIO_08',
outfit = {},
}Lighting
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
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)
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:
{
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:
env.playerSpawnmust 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 itsplayerSpawnis the single most common mistake — the peds then stand in terrain that was never streamed in.- Take the
zof every position from standing on that exact spot, not from a flying camera. Azmore 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:
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:
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
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
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
| Store | VORP | RSG |
|---|---|---|
| Clothing | Copy docs/patches/vorp_character/client/clothingstore.lua | Copy docs/patches/rsg-appearance/client/clothes.lua |
| Barber/Hair | Same 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
| Export | Signature | Description |
|---|---|---|
OpenAppearanceEditor | (opts) -> bool | Start a scoped editor session |
CommitLook | (snapshot) -> bool | Persist + apply + close session |
AbortEditor | () | Close session, revert look |
GetCurrentLook | () -> table|nil | Deep copy {skin, clothes} |
RandomizeAppearance | (scope) | Randomize during active session |
SetEditorMenuVisible | (bool) | Show/hide menu |
IsEditorBusy | () -> bool | Check if editor is free |
ReapplyLook | () | Re-apply saved look |
OpenSecondChance | () | Open SecondChance editor |
OpenVanillaStore | (shopType, storeData?) -> bool | Store 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 | () -> bool | Murphy switch state |
GetMurphyConfig | () -> table|nil | Live 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
| Export | Signature | Description |
|---|---|---|
GetMaxSlots | (src) -> number|nil | Resolved max slots for a player |
registerAction | (name, fn, priority?) | Register hook action |
registerFilter | (name, fn, priority?) | Register hook filter |
spooni_charselector — Client
| Export | Signature | Description |
|---|---|---|
RegisterPreSpawnStep | (fn) -> bool | Step before spawn menu, call done() when finished |
HandlesNewCharSpawn | () -> bool | Internal (RSG): selector owns post-create spawn |
Hooks
-- 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)| Hook | Type | Side | When |
|---|---|---|---|
spooni:beforeEditorOpen | Action | Client | Before menu build |
spooni:afterEditorOpen | Action | Client | After successful menu build |
spooni:beforeSave | Action | Client | Before save (snapshot is read-only) |
spooni:editorClosed | Action | Client | On every editor close |
spooni:afterSave | Action | Server | After successful save |
spooni:afterCreate | Action | Server | First save of a character |
spooni:sectionAllowed | Filter | Client | Every section visibility check |
spooni:editorCategories | Filter | Client | Before applying session categories |
spooni:maxSlots | Filter | Server | Final step of slot resolution |
Practical Examples
Barber NPC:
exports.spooni_charcreator:OpenAppearanceEditor({
id = 'barber', categories = { hairbeard = true },
title = 'Barber', subtitle = 'Hair & Beard',
})Tailor NPC (clothing only):
exports.spooni_charcreator:OpenAppearanceEditor({
id = 'tailor', categories = { clothing = true, dye = true },
title = 'Tailor', subtitle = 'Pick your outfit',
})Eyebrow pencil item:
Config.UseItems = {
{ item = 'eyebrow_pencil', consumable = true,
sections = { 'eyebrows', 'dye' } },
}Dynamic slot bonus for supporters:
-- 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:
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() -- boolServer-side:
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:
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 pedCharacter object shape:
{
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
| Symptom | Cause | Fix |
|---|---|---|
| Two creators open / duplicate peds | Guard missing or not first line | Re-check guard, restart |
| Look reverts after relog | Wrong core detected | Set Config.Framework to your core |
| "No supported core" | Core name/order wrong | Core must start BEFORE spooni, named vorp_core/rsg-core/etc. |
| Item not usable | Inventory caches at boot | Full server restart |
/spooni_check shows FAIL | Guard missing | Re-apply the named guard |
VORP
| Symptom | Fix |
|---|---|
Naked after /rc | Re-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 string | A 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
| Symptom | Fix |
|---|---|
| Naked after selection | Guards swapped. Selector guard = spooni_charselector, Creator guard = spooni_charcreator |
| Clothing store opens vanilla RSG | clothes.lua guard missing or VanillaStoreTakeover.enabled = false |
| A new character is gone after relog | rsg-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 first | Leave 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 relog | playerskins 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
| Symptom | Fix |
|---|---|
| Slot peds naked/blank | spooni_charcreator not running |
| Custom scene: peds float, sink or have no textures | env.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 RSG | rsg-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 shows | Guard missing or not first line |
| Two spawn menus | RSG: Config.SelectLocations = false in rsg-spawn. RPX: don't start rpx-spawn |
| No music | Files must be in html/sounds/ AND listed in sounds.json |
