🎭 Spooni Character Suite
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, 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.
1. Installation
1.1 Requirements
| Resource | Version | Source |
|---|---|---|
jo_libs | v2.12+ | GitHub |
spooni_libs | latest | discord.gg/spooni |
No SQL needed. Database tables are created automatically on first start.
1.2 server.cfg
⚠️ Order is critical. Wrong order = character creation silently breaks.
ensure jo_libs
ensure spooni_libs
ensure spooni_charcreator
ensure spooni_charselector # optional — remove if you don't want the selection sceneCorrect 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_libsmust start after Core + Inventory for the framework bridge.spooni_libsbuilds onjo_libs. Creator needs all three. Selector needs Creator for look previews.
1.3 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 Section 6.
2. Creator Configuration (spooni_charcreator/config.lua)
2.1 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 only2.2 Editor 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).
2.3 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.
2.4 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).
2.5 SecondChance Command
Config.SecondChanceCommand = 'secondchance' -- players type /secondchance turn this false because this is not gated! Use item for players!2.6 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',
}2.7 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
2.8 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,
}2.9 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'2.10 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.
2.11 Intro Animation
Config.Intro = {
enabled = true,
title = 'Start your journey', -- name it whatever you want
duration = 5500,
distMult = 4.0,
font = 22,
}2.12 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+
2.12 Jo_clothingstore & hairdresser
Compatible out of the box just turn off the VanillaTakeover for shops.
3. Selector Configuration (spooni_charselector/config.lua)
3.1 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)3.2 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
}3.3 Ghost Slots (empty places)
Config.Ghost = {
model = 'mp_male',
alpha = 100,
scenario = 'MP_LOBBY_SCENARIO_08',
outfit = {},
}3.4 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,
}3.5 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.
3.6 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' },
}3.7 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) ...
},
}3.8 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.
3.9 Admin Command
Config.AdminCommand = 'charselect' -- '' = disabled
Config.AdminGroups = { 'admin', 'superadmin' }Usage: /charselect <serverId> in server console.
4. 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
4.1 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
},
}4.2 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
4.3 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 (Step 1.3) |
| rsg-barbers (standalone) | — | Copy docs/patches/rsg-barbers/client/barber-guard.lua |
Without these guards, VanillaStoreTakeover does nothing.
5. Developer API
Everything works through exports & hooks — no source access needed. Version 1.5.16.
5.1 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 |
5.2 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 |
5.3 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)6. Custom Framework
Wire a custom core in framework.lua. Use the existing VORP/RSG/RedEM:RP/RPX branches as a reference.
6.1 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)6.2 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', ... }
}7. 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 Step 1.3 |
attempt to call nil (updateCompTints) | Outdated vorp_core — spooni auto-falls-back to DB direct write |
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 |
Selector
| Symptom | Fix |
|---|---|
| Slot peds naked/blank | spooni_charcreator not running |
| 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 |
