Multicharacter Selector Configuration
The selection screen reads config_selector.lua, which is a shared script — the client and the server both load it. Restart the resource after editing:
restart vorp_characterWith Config.DevMode = true in config.lua you can restart the resource without restarting the server, which is how you want to work while you are tuning any of this.
Nothing in this resource is escrowed. Every .lua file is readable and editable, including the ones below.
The config file map
Eight files, each with a single job. Every setting carries a one-line comment in place.
| File | What it controls |
|---|---|
config.lua | Original VORP settings: Lang, the character creator, Config.SpawnPosition (11 locations), Config.DevMode, Config.ReloadCharCommand (default rc) |
config_selector.lua | The selection screen. Everything on this page |
config_scenes.lua | Locations exported from the Scene Creator. 2 ship; ConfigScenes.Enabled = true |
config_scenecreator.lua | The Scene Creator: who may open it, freecam, placement, limits, seats |
animations.lua | The poses players cycle with E, and the duo poses for a partner |
config_emotes.lua | The Scene Creator's own pose browser — 305 poses, separate from animations.lua |
config_shops.lua | Clothing stores (original VORP) |
logs.lua | The Discord webhook for deleted characters |
Your work lives in config files, not in code
Everything a server owner changes sits in config*.lua, animations.lua and logs.lua, plus partner_poses.json if you used the pose editor. The database holds scenes, partners and playtime, and an update does not touch it. So updating is a matter of keeping those files and replacing the rest — there is no code merge to do.
The load order matters and is already set in fxmanifest.lua: config_scenes.lua loads straight after config.lua because it appends to Config.SpawnPosition, and animations.lua loads after config_selector.lua because it writes into ConfigSelector.
General
| Setting | Default | |
|---|---|---|
ConfigSelector.Enabled | true | false brings back the original vorp_menu character list. Also remove the ui_page line from fxmanifest.lua |
ConfigSelector.Debug | false | Extra client and server console output. Leave off on a live server |
ConfigSelector.ScenariosOnly | false | true hides every animation clip and leaves only scenario poses, solo and partner |
ScenariosOnly is the blunt fix for floating characters. Scenarios are grounded by the engine and cannot float; raw clips carry the height of the set they were authored on. The distinction is explained on Emote library.
Playtime, delete, starter items and webhooks
| Setting | Default | |
|---|---|---|
Playtime.enabled | true | Track playtime and show it on the character cards |
Playtime.saveInterval | 300 | Seconds between writes to character_playtime |
Playtime.newLabel | "New" | Shown instead of a time on a character that has never been played |
Delete.enabled | true | Allow deleting characters at all |
Delete.requireNameConfirm | true | The player has to type the character's first name |
Delete.cascade | 14 tables | Rows removed alongside the character |
StarterItems | water ×2, bread ×2 | New characters only. Needs vorp_inventory. false disables it |
StarterWeapons | false | Same, for weapons, e.g. { "WEAPON_MELEE_KNIFE" } |
Webhooks | empty | create, login and logout URLs. An empty URL turns that log off |
Delete.cascade ships pointing at fourteen common VORP tables — character_playtime, outfits, loadout, character_inventories, character_weapons, banking, housing, stables, mailbox, billing, walkanim, metabolism, vorp_mining and vorp_fishing. A table you do not have is skipped rather than throwing an error, so you can leave rows for resources you have not installed. Add your own as { table = "mytable", column = "charidentifier" }.
A webhook URL is a credential
ConfigSelector.Webhooks holds them in plain text. Anyone with the URL can post into that channel. Redact them before sending your config to anyone, point them at a staff-only channel, and rotate one that leaks. The delete webhook is separate and lives in logs.lua.
Scene and camera
ConfigSelector.Scene controls what the screen looks like and how the camera moves between characters.
| Setting | Default | |
|---|---|---|
Scene.allowLocationCycle | true | Players may change location with J |
Scene.showLocationBadge | false | The small "Location N" chip in the corner. Independent of cycling |
Scene.allowPoseCycle | true | Players may change pose with E |
Scene.showOnlySelected | true | Only the highlighted character stands in the scene, with its partner. false is stock behaviour — every character on screen at once |
Scene.interpDuration | 1200 | Milliseconds the camera takes to glide to the next character |
Scene.keepPhotoFilter | false | true keeps the sepia RDO photo filter over the whole selector |
Scene.openEffect | "RespawnPulse01" | Flash played as the screen appears. false disables it |
Scene.focusDistance | 4.0 | Focus distance of the close-up camera |
Scene.motionBlur | 30.0 | Motion blur on the close-up camera. 0 disables it |
showOnlySelected is worth knowing about on a server where players keep six characters. With it on, only one ped is spawned and posed at a time, which is both cheaper and tidier than a row of characters standing in frame.
Depth of field
dof = {
enabled = false,
nearStart = 0.2,
nearEnd = 1.4,
farStart = 4.5,
farEnd = 9.0,
strength = 1.0,
}Test depth of field on your own build before you ship it
It is off by default for a reason. On some RedM builds enabling it turns the character camera completely black. If that happens, set enabled back to false — the five numbers underneath are not the problem.
What the player is allowed to move
ConfigSelector.Camera is a set of permission switches, each one controlling whether a slider appears in the Settings panel. All five default to true.
| Switch | Turns off | Related keys |
|---|---|---|
allowZoom | Background zoom, the field-of-view slider | minFovScale 0.60, maxFovScale 1.40 |
allowCharZoom | Character zoom, the camera-distance slider | minDistScale 0.40, maxDistScale 1.60 |
allowRotate | The rotation slider | rotateStep 15 degrees per step |
allowOrbit | Shift + arrow keys and the orbit and height sliders | orbitStep 5, orbitSpeed 60, heightSpeed 0.6 |
allowFraming | The framing presets | framing list, below |
On both zoom sliders, the middle position is whatever the scene itself configured — 50% on the background slider is the fov from config.lua, and 50% on the character slider is the framing preset's own distance. The scales multiply that, so the ranges above mean the background goes from 40% tighter to 40% wider.
heightMin is -0.6 and heightMax is 2.0, both in metres relative to the framing's own camera height, not to the ground. That is the clamp on Shift + Up and Shift + Down. Widen heightMax if you have built a scene where the interesting angle is from above; tighten both if players keep finding a view through the terrain.
UI defaults, and what players may change
ConfigSelector.UI is the starting state for a player who has never opened Settings. Once they change something it is saved on their own machine, in client KVP, so these values only ever apply to newcomers.
| Setting | Default | |
|---|---|---|
theme | "dark" | "dark" or "light" |
menuStyle | "cinematic" | "cinematic" is the left rail, "classic" the bottom bar |
buttonSkin | "blk_slate" | An id from ConfigSelector.ButtonSkins. Classic bar only |
cinematicBars | true | The letterbox bars, toggled in play with B |
showKeybinds | true | The on-screen key hints |
soundEnabled | true | Interface click and hover sounds |
soundVolume | 0.4 | 0.0 to 1.0, interface sounds only |
musicEnabled | true | The music cue the scene plays |
language | nil | nil follows Lang in config.lua |
credits | two rows | Shown on the Credits page |
The shipped credits table literally reads name = "Change me in config_selector.lua" on its second row. Put your server's name there.
credits = {
{ role = "Framework", name = "VORP Core" },
{ role = "Server", name = "My Server Name" },
},ConfigSelector.Keys holds the rebindable keys as JavaScript key codes with the label the screen draws: pose KeyE, location KeyJ, cinematicBar KeyB, hideUI KeyH, settings KeyO and partner KeyP. Change the code and the label together — the hints follow the table, so a mismatch leaves players reading the wrong letter.
Weather and time
ConfigSelector.Environment decides where the selector's weather and time come from. The order of precedence is the player's choice in Settings, then the scene's own options, then the defaults in this table.
| Setting | Default | |
|---|---|---|
allowPlayerOverride | true | false makes every location always use its own weather and time |
enableWeather / enableTime | true | Whether the selector touches weather and time at all |
defaultWeather | "sunny" | Used when a scene defines nothing |
defaultTime | 12:00 | { hour = 12, minute = 0 } |
weatherTransition / timeTransition | 2.0 / 2 | How long the change takes |
weatherOptions | 22 entries | The dropdown: "scene" plus 21 weathersync types |
snowTypes | 5 types | Weathers that also lay permanent snow |
Weather and time need weathersync. Trim weatherOptions if you would rather players did not put a blizzard behind their character.
The lists you can extend
Four option lists in this file are meant to be added to. Each behaves slightly differently, and one of them will mislead you if you only read the literal table.
38 screen filters
ConfigSelector.Filters.enabled is true, and the literal list holds exactly one row:
ConfigSelector.Filters = {
enabled = true,
list = {
{ id = "none", label = "None" },
},
}The other 38 are appended by a loop immediately below it, from three groups — Game ×18, Modern ×10 and Vintage ×10 — each row built into a photo mode effect name like PhotoMode_FilterGame01:
local groups = {
{ prefix = "Game", label = "Game", count = 18 },
{ prefix = "Modern", label = "Modern", count = 10 },
{ prefix = "Vintage", label = "Vintage", count = 10 },
}Reading the literal table undercounts the filters
There are 38 filters in the dropdown and one none entry, but only the none entry is typed out. To change how many of a group appear, edit its count; to add one of your own, append a row after the loop as { id = "mine", label = "My look", fx = "SomeAnimpostfxName" }.
Filters are applied on the selection screen only and cleared when the player leaves it.
7 music options, 5 of which are real events
ConfigSelector.Music.tracks has seven rows, but two of them are not music:
| Label | event | |
|---|---|---|
| Scene default | nil | Falls through to the location's own options.music |
| Frontier | REHR_START | |
| Character | MP_CHARACTER_CREATION_START | |
| Lobby | MP_CO_OP_LOBBIES_LOW_START | |
| Celebration | MP_CELEB_SCREEN_MUSIC | |
| Drifting | MP_CLOUDS_TRANSITION_MUSIC | |
| Silence | false | Plays nothing |
So five RDR3 music events, plus "whatever the scene asked for" and "nothing at all". The distinction matters when you add a row: an event of nil means defer to the scene and false means stay quiet, so a new track needs a real event name. stopEvent is MC_MUSIC_STOP and is what gets triggered on the way out.
2 button skins, and why Slate ships dimmed
ConfigSelector.ButtonSkins = {
{ id = "blk_slate", label = "Dark slate", file = "blk_slate.webp" },
{ id = "slate", label = "Slate", file = "slate.webp", dim = 0.55 },
}These are the plates behind the buttons of the classic bottom bar; the cinematic layout does not use them. file is relative to html/img/btn/, which contains exactly these two files.
dim is optional and defaults to 1. Slate ships at 0.55 because at full strength the plate drops the PLAY accent's contrast against it to roughly 2.0:1, which is not readable. If you add a light plate of your own, expect to need a dim value too.
A new plate needs two lines, not one
A row in ConfigSelector.ButtonSkins and a line in the files block of fxmanifest.lua. Without the second, the image is never sent to the client and the button renders bare. Use uniform materials — badge-shaped images stretch badly across the different button widths.
5 camera framing presets
ConfigSelector.Camera.framing is the Framing dropdown. Each row aims the camera at a skeleton bone, from in front of the character.
| Label | bone | look | dist | camLift |
|---|---|---|---|---|
| Scene default | — | — | — | — |
| Full body | 14410 | 1.00 | 2.60 | 0.25 |
| Upper body | 14413 | 1.35 | 1.40 | 0.02 |
| Head | 21030 | 1.62 | 0.85 | 0.00 |
| Legs | 55120 | 0.45 | 1.60 | 0.35 |
bone is an RDR3 skeleton bone id — skel_spine0 at the waist, skel_spine3 at the upper chest, skel_head, skel_l_calf at the shin. dist is how far in front of the character the camera sits in metres, camLift is the camera's height relative to the aim point, and look is a fallback height above the feet used when the bone cannot be read. Scene default carries no values at all: it leaves the camera exactly where the location configured it.
For a paired character the camera moves side-on instead, so both people are in frame.
Why Logout ships off
ConfigSelector.Logout = {
enabled = false,
command = "logout",
cooldown = 60,
canLogout = function(source)
return true
end,
}Switching character without reconnecting is the one feature in this file that is disabled by default, and it is not a performance decision.
Logout needs a patch to vorp_core, not just a true
It calls a logoutCharacter() function in vorp_core/server/class/user.lua. Stock vorp_core does not have that function. Setting enabled = true without adding it registers the command and then fails when a player runs it. Leave it off unless you have patched vorp_core yourself and understand that your patch has to be reapplied every time you update the framework.
If you do run it, the two settings around it are worth setting properly. cooldown is the number of seconds a player has to be in-game before they may log out again — 60 stops slot-hopping being used to dodge something. canLogout(source) runs on the server and blocks the logout if it returns false, which is where you check whether the player is in jail, in combat or cuffed:
canLogout = function(source)
if exports.my_jail:isJailed(source) then return false end
return true
end,command = false registers no command at all, leaving the logout button in the interface as the only route in.
Covered elsewhere
Three parts of this file are large enough to have their own page, and they are not repeated here.
- Locations —
Config.SpawnPosition,ConfigScenes.List, and the 13 locations that ship. - Poses —
animations.lua, thesourcemodes,perLocation, and the 20 duo poses. - Languages —
ConfigSelector.Locales, the 7 locales at 89 keys each, and how to add an eighth.
If something in the interface is not behaving the way this page says it should, turn ConfigSelector.Debug on, reproduce it, and read the console before changing anything else. Failing that, there is a Discord.