The Partner System
VORPA pairing ties two characters together, not two accounts. Once it exists, the partner is staged beside the character on the selection screen, dressed as that character, and the two share one pose.
Everything on this page lives in ConfigSelector.Partner (config_selector.lua), with the duo poses themselves in animations.lua. All of it is plain readable Lua.
Pairing two characters
The panel opens with P on a character slot. The key is ConfigSelector.Keys.partner, and the on-screen hint follows whatever you set it to. With Partner.enabled = false the button, the hint and the key all disappear.
- Both players open the character screen. This is not optional — the panel only lists players who have it open right now.
- One of them presses P and types the other player's name, or their server ID, into the search box. The list holds player names as the server sees them, not character names, and it matches on any part of the name. An ID has to match exactly.
- On the row that comes back, press Couple or Friend. The request is sent for the character that is currently highlighted.
- The other player gets a notification and a count on the Incoming tab. They accept or decline.
- On accept, both sides get a fresh character list and the scene restages. The partner appears without anyone reopening the screen.
Both halves use the character highlighted at that moment
Sending binds the character you were on when you pressed Couple or Friend, and accepting binds the character the accepting player is on. Someone who scrolls to another slot and then accepts has paired that slot instead. Highlight the character you mean before you answer.
The panel has three tabs:
| Tab | Holds |
|---|---|
| Search | Every other player at the character screen, sorted by name, with the Couple and Friend buttons |
| Incoming | Requests sent to you, with Accept and Decline. The tab carries a count badge |
| Sent | Requests you have sent, with Withdraw |
The panel's subtitle shows your own server ID. That is the number to read out when someone wants to search for you by ID rather than by name.
Unpair sits at the top of the panel whenever the highlighted character has a partner. It ends the pairing from either side, and the other player's screen updates too if they are on it.
One partner per character
char_a and char_b are both unique keys in the database, so a character can be in exactly one pairing. A player with five characters can pair all five, each with somebody different.
While a character already has a partner, the search rows show Already partnered instead of the two buttons. Accepting a request that arrived before a pairing was made is re-checked on the server and refused — the request is dropped rather than quietly overwriting what exists.
Why a request is refused
Every send and every accept is validated on the server. The panel plays its error sound and re-reads the state; the reason is in the response:
| Reason | What happened |
|---|---|
not_at_selector | The other player is online but not on the character screen |
offline | They left between the list being built and the button being pressed |
already | One of the two characters was paired in the meantime |
duplicate | You already have an open request to that player |
not_yours | The character identifier sent did not belong to the sender |
type | The requested type is not in allowedTypes |
self | The target was the sender |
Requests are held in memory, not in the database. A restart clears them, and so does either player disconnecting. Expiry is swept every 5 seconds, and the sender's panel refreshes when one lapses.
What pairing changes on screen
| The partner ped | Built from the partner character's own skinPlayer, compPlayer and compTints rows, so it is that character with that character's clothes |
| Online or not | The data comes from the database. The partner shows up whether or not that player is connected |
| E | Cycles the duo poses instead of the character's solo poses |
| The pose picker | Lists the duo poses for that pairing type, with the current one marked |
| The camera | Frames both people: a side-on shot aimed at the middle of the pair, stepping back until both fit with 0.45 m of room either side |
| The card | Carries a heart for a couple and a dot for a friend, and the stats panel names the partner |
The partner ped is placed, never attached. Attaching would make it inherit the character's height, and the resource works in foot-level terms deliberately. It is also invincible, cannot be targeted, and ignores non-temporary events, so nothing in the world walks it off its mark.
The last duo pose is remembered per character in the client's own settings, so reopening the screen brings back the pose that character was last seen in.
Paired characters only turn while they are on screen
The rotation setting in Settings turns every staged character. A paired one is skipped unless it is the highlighted slot, because moving the partner to match cancels the clip it is running. Turn the slot you are looking at and the partner swings round with it; the camera re-aims afterwards.
With ConfigSelector.Scene.showOnlySelected at its default of true, a partner is visible exactly when its own character is — so you see one couple at a time, not the whole rotation.
Settings
-- config_selector.lua
ConfigSelector.Partner = {
enabled = true,
requestTimeout = 60,
allowedTypes = { "lover", "friend" },
sync = true,
freeOffset = vector4(0.85, 0.35, 0.0, 200.0),
freePoseSource = "global",
allowPropPoses = true,
clearOnDelete = true,
}| Setting | Default | Does |
|---|---|---|
enabled | true | The whole system. false hides the P button and the hint, and the server refuses sends, replies and unpairs |
requestTimeout | 60 | Seconds a sent request stays open |
allowedTypes | { "lover", "friend" } | Which pairing types the server accepts |
sync | true | One shared duo pose, or a partner that poses on its own. See below |
freeOffset | vector4(0.85, 0.35, 0.0, 200.0) | Where a free partner stands. Read only when sync = false |
freePoseSource | "global" | "global" uses ConfigSelector.Animations.list; "character" uses the character's own list. sync = false only |
clipOffsets | 2 entries | Per-clip height corrections, in metres. Positive lowers the ped |
allowPropPoses | true | Keep duo poses marked needsProp in the list |
clearOnDelete | true | Wipe the pairing when either character is deleted |
editor | enabled, command, groups, ace | The partner pose editor. See Pose editor |
allowedTypes is enforced on the server only
The panel always draws both the Couple and the Friend button. Cut "lover" from allowedTypes and the Couple button is still there — it just fails with the error sound when pressed. If you want one type gone from the interface, remove its button from the page as well.
What sync actually switches
sync = true (default) | sync = false | |
|---|---|---|
| Pose source | ConfigSelector.Partner.animations, by pairing type | The solo list named by freePoseSource |
| Placement | The pose's own attach, or the type default | freeOffset, the same spot for every pose |
| Contact | Dances, bows and kisses line up | None. Two people standing near each other |
| What E moves | Both halves, onto the next duo pose | The partner's own pose. The character keeps the pose its slot staged |
sync = false is the safer setting on a location whose marks are tight or uneven: a free partner never needs its clip to line up with the character's, so nothing can be visibly out by half a metre. A missing sync key counts as true.
The attach defaults
Where the partner stands, for a duo pose that does not carry its own attach:
-- animations.lua
ConfigSelector.Partner.attach = {
lover = vector4(0.0, 0.60, 0.0, 180.0),
friend = vector4(0.0, 1.05, 0.0, 180.0),
}The vector is right, forward, up, heading, measured in the character's own space. A couple stands 0.60 m in front, a friend 1.05 m in front, and heading 180 turns the partner to face your character. Every shipped duo pose carries its own attach, so these two are the fallback for poses you add yourself.
Where the duo poses come from
ConfigSelector.Partner.animations in animations.lua holds two lists, picked by pairing type: 8 poses for lover and 12 for friend, 20 in total. Three kinds appear in them:
| Type | What plays | Contact |
|---|---|---|
emote | Two clips from one dictionary, played together | Yes. This is the only kind with real contact, such as the slow dance |
kit | An RDO emote kit action on each ped, replayed on repeatMs | No |
scenario | A world scenario on each ped | No |
Several entries hold female variants (primaryFemale, partnerFemale), and the slow dance carries swapByGender = true so a female character takes the woman's track and her partner the man's.
Two filters sit in front of the list. allowPropPoses = false drops entries marked needsProp = true — the shipped At the railing and Campfire friend poses were authored around furniture the spawn marks do not have, and flagging them is how you retire them without deleting them. ConfigSelector.ScenariosOnly = true drops every pose that is not a scenario on both halves, which is the blunt fix for floating.
The full list, the entry shapes and the mechanics of playing two clips in step are on Poses. To move a partner by hand instead of guessing numbers, use the Pose editor: it writes partner_poses.json, which overrides the attach values written here and reaches every player at once.
This is not the Scene Creator's library
The 305-pose browser documented in Emote library belongs to the Scene Creator and has its own paired category. Adding a row there does not put a pose in front of players pressing E.
The data
One table, created with CREATE TABLE IF NOT EXISTS when the resource starts. There is no SQL file to import.
CREATE TABLE IF NOT EXISTS `character_partners` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`char_a` INT(11) NOT NULL,
`char_b` INT(11) NOT NULL,
`type` VARCHAR(16) NOT NULL DEFAULT 'friend',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_char_a` (`char_a`),
UNIQUE KEY `uniq_char_b` (`char_b`)
);| Column | Holds |
|---|---|
char_a | The character that sent the request. Unique |
char_b | The character that accepted. Unique |
type | lover or friend |
A row is read in both directions, so it does not matter which side of it a character sits on. The unique keys are what enforce one partner per character — a second pairing cannot be inserted even if something tries.
With clearOnDelete = true, deleting a character removes the row it appears in, on either side. The other character is left unpaired and free to pair again. Set it to false and the row survives the deletion, which leaves a pairing pointing at a character that is gone.
If the CREATE fails, the partner system turns itself off for that session and prints the error on the server console rather than failing on every request.
Pairings are the only partner data in the database. The per-character last pose is client-side, in the KVP settings, and pose editor saves live in partner_poses.json in the resource folder — see Database.
Troubleshooting
The two players cannot find each other
The search list is built from players who have the character screen open at that moment. Nobody who is in the world, in the character creator, or on a loading screen appears in it.
- Both players press Esc or Backspace out to the character screen and leave it open.
- Re-open P. The list is rebuilt from scratch each time the panel opens.
- Compare server IDs rather than names. The subtitle of each player's own panel shows their ID, and an ID search has to match exactly.
- If one player sees the other but not the reverse, the one who cannot see is probably looking at a stale panel — close it and open it again.
A request also lapses after requestTimeout seconds, 60 by default. Nothing is wrong if a request simply vanished while the other player was reading it; send it again, or raise the timeout.
The partner floats or sinks
A duo pose that plays a raw clip brings the height of the set it was authored on with it. The resource measures it and tells you the number instead of moving the ped, because moving a ped cancels its task.
- Set
ConfigSelector.Debug = trueand restart the resource. - Open the character screen on the paired character and put it in the pose that looks wrong.
- Run
/selectordumpin the F8 console. Use/selectoroverlayfor the same reading live on screen while you step poses with/selectorposenext. - Read the partner section of the dump.
-- heights (the bug lives here) --
partner foot z -0.619
partner gap 0.420
VERDICT FLOATING 0.42m ABOVE the ground
FIX ["script_re@roaming_thugs@laramie/thug_b_argue_idle"] = 0.420,The FIX line is ready to paste into clipOffsets:
clipOffsets = {
["KIT_EMOTE_GREET_FANCY_BOW_1"] = 0.128,
["MP_LOBBY_SCENARIO_06"] = 0.116,
["script_re@roaming_thugs@laramie/thug_b_argue_idle"] = 0.420,
}A positive value lowers the ped. The key is looked up as dictionary/clip first and then as the clip name on its own, which is what makes a bare key apply to that clip in any dictionary. Scenarios and kit emotes have no dictionary, so they are keyed by their own name — the two shipped entries above are one of each. The table lives under Partner but is read for solo character poses too.
The alternative is the Pose editor, which is the better tool when the partner is standing at the wrong distance or angle rather than the wrong height: you nudge the ped with the arrow keys, press Enter, and the saved position reaches every player immediately.
A hovering character, rather than a hovering partner
Then the pose is a solo one and clipOffsets still applies, but start at Troubleshooting for the full sequence.
Nothing happens when P is pressed
In order:
- Is
Partner.enabledstilltrue? When it isfalsethe button is hidden rather than disabled. - Is a character actually highlighted? The panel needs one — it does not open on an empty slot.
- Did the
character_partnerstable get created? A database error turns the system off for the session and prints on the server console. - Has
ConfigSelector.Keys.partnerbeen changed to something else? The on-screen hint shows the key that is really bound.
Permissions for the pose editor are separate from all of this: VORP groups in Partner.editor.groups, default admin, or the ace in Partner.editor.ace, default command, which txAdmin admins already hold. Groups come from the character's group — see VORP.
Still stuck, with the dump output in hand, ask on Discord.