> For the complete documentation index, see [llms.txt](https://bedoflexdev.gitbook.io/development/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://bedoflexdev.gitbook.io/development/scripts/bedoflex-multicharacter.md).

# bedoflex Multicharacter

bedoflex-multicharacter Documentation

Cinematic character selection, registration and spawn selector for Qbox, QBCore and ESX Legacy, with a built-in radio, per-character scenes and poses, status badges, duo scenes, extra slots and Tebex slot sales.

{% embed url="<https://youtu.be/Y5YxrvJ5w6c>" %}

## ------FEATURES------

* **Cinematic selection.** Scripted cameras with depth of field and sway, 12 framings from close-up to establishing shot, 26 scenes across the map with their own time and weather, scenes per job or gang, a head tag that follows the character, letterbox and film grain. Characters swap instantly, every look is preloaded.
* **Registration.** First and last name, date of birth, nationality with search, height and backstory, with a live ID card preview. Blocked words, unique names and a create cooldown are configurable.
* **Per character look.** Pose (50+ poses: standing, gestures, dances, sport, sitting), framing, scene, filter and swap effect are picked per character and saved in the database, so they follow the character to any PC. Props from a pose (phone, cup, cigarette) are cleaned up when the pose changes.
* **bedoflex FM.** A radio in the top bar that plays the music files you add to the resource, with a seek bar, playlist, shuffle, repeat, volume and synced lyrics as cinematic subtitles. Title, artist and cover are read from the mp3 tags. Lyrics can be shifted per song.
* **Spawn selector.** Sharp aerial preview with a native sky transition, last location, job / gang locked spawns, or hand off to qb-spawn, qbx\_spawn, apartments and housing scripts. Loading a character and the first spawn view happen behind a black screen, so players never see a hitch.
* **Particle effects.** An effect when switching characters and when a new character lands in the city. Players pick their own swap effect per character.
* **Status badges.** Players pick a status per character (New in town, Looking for RP...) or write their own. Ace gated entries for Streamer, VIP and more.
* **Duo scene.** Two players link their characters and stand together on the selection screen as a couple or friends. 36 duo poses (holding hands, embrace, slow dance, bro hug, handshake, sparring, picnic and more) and 14 duo locations with their own time and weather. Both characters turn together when dragged.
* **Slots.** Default slots plus identifier, ace, Discord role, admin command and Tebex purchases, with locked slots shown to players.
* **Player settings.** Four tabs: Character (pose, framing, scene, filter, swap effect, status badge), View (camera, scene, weather, time, effects), Audio (radio, lyrics, interface sounds) and Interface (accent color, character tag, streamer mode).
* **Shortcuts.** `P` next pose, `J` next scene, `H` photo mode, `B` cinematic bars, `M` music. In a duo scene `P` and `J` cycle duo poses and duo locations.
* **Secure.** Server-side validation for every action, routing bucket isolation, rate limits, and logout is blocked while dead, cuffed or jailed.
* **Open hooks.** Everything that talks to other resources (inventories, id cards, emote menus) lives in open files, ready to adapt.
* **Languages.** English, Turkish, German, French, Spanish, Portuguese, Polish, Dutch and Arabic.

## ------REQUIREMENTS------

| Requirement                                        | Note                               |
| -------------------------------------------------- | ---------------------------------- |
| [ox\_lib](https://github.com/overextended/ox_lib)  | Required                           |
| [oxmysql](https://github.com/overextended/oxmysql) | Required                           |
| Framework                                          | qbx\_core, qb-core or es\_extended |
| OneSync                                            | Required                           |

Supported clothing: illenium-appearance, fivem-appearance, bl\_appearance, rcore\_clothing, crm-appearance, tgiann-clothing, origen\_clothing, qb-clothing and skinchanger.

## ------INSTALLATION------

## STEP 1 - Unzip The Folder

Unzip the script folder, then copy or cut everything inside it and paste it into your server resources folder.

## STEP 2 - Add Script To Server

Start it after your framework, inventory, clothing, emote, apartment and spawn resources:

```
ensure ox_lib
ensure oxmysql
ensure qbx_core
ensure bedoflex-multicharacter
```

Remove or stop any other multicharacter resource. `basic-gamemode` is stopped automatically, remove it from your server config anyway.

**NOTE: Please don't change the script name or folder name.**

## STEP 3 - Framework Setup

{% tabs %}
{% tab title="Qbox" %}
In `qbx_core/config/client.lua`:

```lua
characters = {
    useExternalCharacters = true,
    startingApartment = false, -- use spawn.newCharacter = 'apartment' in this script instead
}
```

{% endtab %}

{% tab title="QBCore" %}
Remove `qb-multicharacter` from your resources.
{% endtab %}

{% tab title="ESX" %}
Remove `esx_multicharacter` and `esx_identity`, then set this in `es_extended/config.lua`:

```lua
Config.Multichar = true
```

The character prefix (`char1:license`) is set with `esx.prefix` in `config/config.lua`.
{% endtab %}
{% endtabs %}

{% hint style="info" %}
If your loading screen does not use `loadscreen_manual_shutdown 'yes'`, add it so the loading screen stays up until the selection is ready.
{% endhint %}

## STEP 4 - Database

There is nothing to import. These tables are created and updated on start:

| Table                    | Holds                                                  |
| ------------------------ | ------------------------------------------------------ |
| `bfx_multichar_slots`    | Extra slots per license                                |
| `bfx_multichar_tebex`    | Tebex transactions                                     |
| `bfx_multichar_partners` | Duo scenes                                             |
| `bfx_multichar_profiles` | Height, backstory, status badge and per character look |

## STEP 5 - Script Configuration (`config/config.lua`)

### STEP 5.1 - General & Interface

```lua
locale = 'en',              -- en, tr, de, fr, es, pt, pl, nl, ar or 'auto' (follows ox:locale)
framework = 'auto',         -- 'auto', 'qbx', 'qb' or 'esx'
debug = false,
manualStart = false,        -- true: selection waits for TriggerEvent('bedoflex-multicharacter:client:start')
idleKick = 0,               -- minutes idle in selection before a kick, 0 to disable

ui = {
    serverName = 'bedoflex',
    serverTag = 'roleplay',
    logo = '',                  -- empty uses the bd mark, or an image url
    storeUrl = 'https://bedoflexdev.tebex.io',
    discordUrl = '',
    currency = '$',
    showCitizenId = true,
    showPhone = true,
    worldTag = true,            -- small label that follows the character's head
    -- defaults players can change in settings
    letterbox = false,
    grain = true,
    sway = true,
    dof = true,
    holdToDelete = true,        -- hold the button to delete instead of a single click
    sounds = true,
    accent = '#e5262f',         -- default color, players can pick another one
    accents = { '#e5262f', '#ff6a1a', '#f2b33d', '#3ba55d', '#14b8a6', '#3b82f6', '#8b5cf6', '#ec4899', '#f4f2ef' },
    customAccent = true,        -- allow any color from a picker
    credits = true,             -- team window, edit config/credits.lua
},
```

The whole interface, the character tag, the spawn marker and the ID card follow the accent color. Text on light accents switches to dark on its own.

### STEP 5.2 - Particle Effects

```lua
ui = {
    particles = {
        swap = 'smoke',             -- when switching to a character, false to turn off
        newCharacter = 'confetti',  -- when a new character lands in the city, false to turn off
        list = {
            { id = 'smoke', label = 'Smoke', asset = 'scr_rcbarry2', name = 'scr_clown_appears', scale = 0.6 },
            { id = 'confetti', label = 'Confetti', asset = 'scr_xs_celebration', name = 'scr_xs_confetti_burst', scale = 1.2 },
            -- money, clown, beast, gumballs, fireworks ...
        },
    },
},
```

Every entry in `list` shows up in the player settings, so players can pick their own swap effect per character. Add any particle with its `asset` and `name`.

### STEP 5.3 - Camera & Scenes

```lua
camera = {
    minFov = 14.0,
    maxFov = 56.0,
    angle = 22.0,           -- degrees the camera sits off the character's facing direction
    subject = 0.24,         -- pushes the character left of centre, 0 keeps it centred
    introDuration = 3400,
    swapDuration = 800,
    defaultFraming = 'full',
    framings = {
        -- angle: extra degrees around the character
        closeup = { dist = 0.95, height = 0.62, look = 0.62, fov = 26.0 },
        portrait = { dist = 1.45, height = 0.55, look = 0.55, fov = 30.0 },
        waist = { dist = 1.9, height = 0.42, look = 0.38, fov = 30.0 },
        profile = { dist = 2.2, height = 0.38, look = 0.34, fov = 31.0, angle = 62.0 },
        medium = { dist = 2.35, height = 0.35, look = 0.3, fov = 32.0 },
        low = { dist = 2.7, height = -0.55, look = 0.38, fov = 36.0, angle = -10.0 },
        shoulder = { dist = 2.9, height = 0.6, look = 0.45, fov = 30.0, angle = 128.0 },
        high = { dist = 3.1, height = 2.3, look = -0.15, fov = 36.0 },
        full = { dist = 3.7, height = 0.2, look = -0.05, fov = 34.0 },
        cinematic = { dist = 4.8, height = -0.15, look = 0.45, fov = 26.0, angle = 34.0 },
        wide = { dist = 5.6, height = 0.7, look = -0.1, fov = 38.0 },
        establishing = { dist = 9.0, height = 2.6, look = 0.2, fov = 40.0, angle = 18.0 },
    },
},

transition = 'switch',      -- 'switch' native sky transition, 'fade' plain fade

scenes = {
    random = true,          -- random scene each time, false: first matching scene
    playerChoice = true,    -- players can pick a scene in settings
},
```

Every framing shows up in the player settings, the names come from the locale (`framings.<name>`). Add your own with `dist`, `height`, `look`, `fov` and an optional `angle`.

Scenes themselves are set in `config/scenes.lua`. A scene with `groups` is only used for characters with that job or gang, `hour` and `weather` give a scene its own time and weather. Characters are placed on the ground on their own, so a `z` that is a little off in your own scenes is fine.

### STEP 5.4 - Selection Weather & Time

```lua
environment = {
    enabled = true,
    weather = 'EXTRASUNNY',
    hour = 19,
    minute = 30,
    sync = function(state) ... end,   -- pauses your weather sync while the selection is open
},
```

qb-weathersync, Renewed-Weathersync, cd\_easytime and vSync are paused automatically. Add your own weather script inside `sync`. Players can change weather and time for themselves in settings.

### STEP 5.5 - Slots

```lua
slots = {
    default = 3,
    max = 10,          -- hard cap, extra slots never go above this
    showLocked = 2,    -- locked slots shown so players see what they can unlock
},
```

Extra slots stack from `config/server.lua` (identifiers, aces and Discord roles), the admin command and Tebex purchases.

### STEP 5.6 - Registration

```lua
register = {
    name = { min = 2, max = 16 },
    dob = { minYear = 1940, maxYear = 2008, format = 'DMY' },   -- 'DMY', 'MDY' or 'YMD'
    height = { enabled = true, min = 150, max = 210 },
    backstory = { enabled = true, required = false, min = 20, max = 400 },
    uniqueNames = false,
    blockedNames = { 'admin', 'moderator', 'staff', 'owner', 'developer' },
    cooldown = 0,                    -- minutes between creating characters
    autoOpenForNewPlayers = true,    -- first character opens registration directly
},
```

### STEP 5.7 - Delete

```lua
delete = {
    enabled = true,
    everyone = true,                    -- false: only deleteAccess in config/server.lua or the ace below
    ace = 'bedoflex.multichar.delete',
    typeToConfirm = true,               -- player types the character name to confirm
},
```

Tables cleared when a character is deleted are listed in `config/delete.lua`.

### STEP 5.8 - Status Badges & Duo Scene

```lua
badges = {
    enabled = true,
    list = {
        { id = 'new', label = 'New in town', color = '#3ba55d' },
        { id = 'lfrp', label = 'Looking for RP', color = '#3b82f6' },
        -- ace gated: add_ace group.streamer bedoflex.multichar.badge.streamer allow
        { id = 'streamer', label = 'Streamer', color = '#8b5cf6', ace = 'bedoflex.multichar.badge.streamer' },
    },
    custom = { enabled = true, max = 18, ace = false },   -- free text, filtered with register.blockedNames
},

partner = {
    enabled = true,
    distance = 6.0,         -- max distance between players to send a duo invite
},
```

Duo poses and locations are set in `config/duo.lua`. While a character has a duo, the pose and scene pickers in settings show duo poses and duo locations, and dragging turns both characters together.

```lua
-- config/duo.lua
poses = {
    -- paired animation on one origin, m / f roles for couples
    { id = 'hands', label = 'Holding hands', type = 'couple', sync = { dict = 'timetable@trevor@ig_1', m = 'ig_1_thedontknowwhy_trevor', f = 'ig_1_thedontknowwhy_patricia' } },
    -- own animation per character, at = partner offset (right, forward, up), face = how much they turn to each other
    { id = 'drinks', label = 'Drinks', a = { scenario = 'WORLD_HUMAN_DRINKING' }, b = { scenario = 'WORLD_HUMAN_DRINKING' }, at = vec3(1.0, 0.45, 0.0), face = 0.6 },
},
scenes = {
    -- same fields as config/scenes.lua, pose = default duo pose per type
    { label = 'Diamond Casino', coords = vec4(924.33, 46.15, 81.1, 58.0), hour = 23, pose = { couple = 'embrace', friends = 'cheer' } },
},
```

`type` limits a pose to `couple` or `friends`, leave it out for both. A duo location needs about two meters of open ground to the character's right. If the camera ends up behind a wall, lower `camera.distance` for that location.

### STEP 5.9 - Spawn

```lua
spawn = {
    mode = 'selector',        -- 'selector', 'last' or 'external' (qb-spawn, qbx_spawn, okok, vms)
    newCharacter = 'selector',-- 'selector', 'apartment' or 'default'
    defaultCoords = vec4(-1037.74, -2737.84, 20.17, 328.0),
    lastLocation = true,
},
```

Spawn points are set in `config/spawns.lua`. A spawn can be limited to jobs or gangs with `groups`, and to new or existing characters with `new`.

### STEP 5.10 - Starter Items

```lua
starterInventory = 'auto',
starterItems = {
    { item = 'phone', amount = 1 },
    { item = 'id_card', amount = 1 },
    { item = 'driver_license', amount = 1 },
},
```

`auto` finds ox\_inventory, qs-inventory, codem-inventory, tgiann-inventory, ps-inventory, lj-inventory or qb-inventory, otherwise the framework adds the items. Items are given once the new character's inventory has loaded. `id_card` and `driver_license` go through qbx\_idcard, bl\_idcard or um-idcard when one of them runs.

### STEP 5.11 - Hud & Clothing Menu

```lua
-- hide / show your hud while in selection
hud = function(visible)
    -- exports['your-hud']:toggle(visible)
end,
```

`firstAppearance` opens the clothing menu for a freshly created character. It works with qb-clothing / illenium-appearance and esx\_skin out of the box.

### STEP 5.12 - Other Config Files

| File                       | Purpose                                                            |
| -------------------------- | ------------------------------------------------------------------ |
| `config/server.lua`        | Extra slots, account badges, delete access, Tebex, logs, migration |
| `config/scenes.lua`        | Selection scenes, job / gang scenes, camera overrides              |
| `config/poses.lua`         | Poses players can pick per character                               |
| `config/duo.lua`           | Duo scene poses and locations                                      |
| `config/spawns.lua`        | Spawn selector locations                                           |
| `config/music.lua`         | Radio settings, tracks and lyrics                                  |
| `config/delete.lua`        | Tables cleared when a character is deleted                         |
| `config/nationalities.lua` | Nationality list                                                   |
| `config/credits.lua`       | Team window members                                                |

## STEP 6 - Start The Server

Restart your server and join. The selection screen opens as soon as you load in.

## ------BEDOFLEX FM------

```lua
-- config/music.lua
enabled = true,
station = 'bedoflex FM',
autoplay = true,
shuffle = false,
repeatMode = 'all',     -- 'all', 'one' or 'off'
volume = 0.25,
lyrics = true,          -- show lyrics as subtitles
```

Tracks are music files you put in `web/audio`:

```lua
tracks = {
    'audio/song.mp3',
    { url = 'audio/song.mp3', title = 'Name', artist = 'Artist', cover = 'img/cover.jpg' },
},
```

* **Names and covers.** Title, artist and cover of your tracks are read from the mp3 tags on the server, so they show before a song plays. Files without tags use the file name, `Artist - Title.mp3`. Embedded covers above \~150 KB are skipped since every player downloads the list, use `cover = 'img/cover.jpg'` for big ones.
* **Lyrics.** Paste plain lines on a track (spread over the song on their own), add an `.lrc` file from `web/audio` for exact timing, set `offset` in seconds for a version with a longer or shorter intro, or set `lyrics = false` for a track.
* **Lyric sync.** The `-` / `+` buttons next to the lyrics button shift the lyrics for that song and start it again from the beginning. Clicking the number between them resets it. The shift is saved per song on the player's PC.

{% hint style="warning" %}
Only use music you own or have permission to use. Music added to the radio is the server owner's responsibility.
{% endhint %}

## ------EXTRA SLOTS & TEBEX------

**Identifiers and aces** (`config/server.lua`):

```lua
identifierSlots = {
    ['license:xxxxxxxx'] = 2,
},
aceSlots = {
    { ace = 'bedoflex.multichar.slots.vip', slots = 2 },   -- add_ace group.vip bedoflex.multichar.slots.vip allow
},
```

**Admin command:** `/charslots <id or license> <amount>` (`3` sets, `+1` adds, `-1` removes).

**Discord roles:** add the roles to `discordSlots` in `config/server.lua` and these convars to your server config:

```
set bedoflex:discordToken "bot token"
set bedoflex:discordGuild "guild id"
```

Roles are checked once every 10 minutes per player. When Discord rate limits the bot after a restart, players keep their last known slots instead of losing them.

**Tebex:** set `tebex.enabled = true` in `config/server.lua`, then add this game server command to your Tebex package:

```
bfxmc_addslots {"transaction":"{transaction}","slots":1}
```

Players redeem their transaction id from a locked slot in game. If you rename the command, change `tebex.command` too.

## ------ACCOUNT BADGES & LOGS------

Small badges next to the character name, set in `config/server.lua`. Ace and identifier badges show on every character of that player, job badges only on characters with that job or gang.

```lua
badges = {
    { label = 'Staff', color = '#e5262f', ace = 'bedoflex.multichar.badge.staff' },
    { label = 'VIP', color = '#f2b33d', identifiers = { 'license:xxxxxxxx' } },
    { label = 'LSPD', color = '#3b82f6', jobs = { 'police' } },
},
```

`logs` sends create, play, delete, logout, slot changes and blocked exploit attempts to Discord webhooks, one per event (`service = 'discord'`), or the ox\_lib logger (`service = 'ox'`).

## ------COMMANDS------

| Command                               | Who     | Description                                              |
| ------------------------------------- | ------- | -------------------------------------------------------- |
| `/logout [id]`                        | Admin   | Back to character selection                              |
| `/charslots <id or license> <amount>` | Admin   | Set extra character slots                                |
| `/chardelete <citizenid>`             | Admin   | Delete an offline character                              |
| `/duo <id> [couple / friends]`        | Players | Invite a nearby player to your selection scene           |
| `/duoremove`                          | Players | Remove your duo scene                                    |
| `charmigrate`                         | Console | Renumber character slots per license (QBCore / Qbox)     |
| `charmigrate stories`                 | Console | Copy backstories and heights from another multicharacter |

Command names and permissions are set in `commands` in `config/config.lua`. A player's own logout is blocked while dead, cuffed or jailed and has a cooldown (`commands.logout.cooldown`).

## ------MIGRATING FROM ANOTHER MULTICHARACTER------

1. Install the script as above and remove the old multicharacter.
2. Run `charmigrate` in the server console to fix duplicated or missing slot numbers (QBCore / Qbox).
3. Run `charmigrate stories` to bring backstories and heights over.

Story sources are listed in `config/server.lua` under `migrate`. By default the script looks in `players.charinfo`, `players.metadata` and the ESX `users` table. If your old script kept stories in its own table, add a line:

```lua
migrate = {
    stories = {
        { table = 'old_script_table', id = 'citizenid', story = 'story' },
    },
},
```

Stories that already exist in this script are never overwritten.

## ------HOOKS (OPEN FILES)------

Everything that talks to other resources is in `hooks/` and is not escrowed, so it can be adapted to any server.

| File               | Purpose                                                                      |
| ------------------ | ---------------------------------------------------------------------------- |
| `hooks/server.lua` | Starter items for any inventory, id cards, logout rules, after create / load |
| `hooks/client.lua` | Emote menu, spawnmanager, particle effects                                   |

```lua
-- hooks/server.lua
function Hooks.canLogout(src)        -- return false to block a player's own logout
function Hooks.created(src, citizenid, data)  -- runs after a character is created
function Hooks.loaded(src, citizenid)         -- runs after a character is loaded
```

## ------EXPORTS & EVENTS------

**Client**

```lua
exports['bedoflex-multicharacter']:open()     -- open the selection
exports['bedoflex-multicharacter']:isOpen()   -- true while it is open

-- fires once the player is in the world
AddEventHandler('bedoflex-multicharacter:client:spawned', function(isNew) end)

-- with manualStart = true, the selection waits for this (queue / custom intro)
TriggerEvent('bedoflex-multicharacter:client:start')
```

**Server**

```lua
exports['bedoflex-multicharacter']:logout(source)
exports['bedoflex-multicharacter']:getCharacters(source)
exports['bedoflex-multicharacter']:getSlots(source)
exports['bedoflex-multicharacter']:addSlots(license, amount)
exports['bedoflex-multicharacter']:deleteCharacter(citizenid)
```

## ------EDITABLE FILES------

| File                   | Purpose                           |
| ---------------------- | --------------------------------- |
| `config/*.lua`         | Every setting                     |
| `hooks/*.lua`          | Integrations with other resources |
| `bridge/**`            | Framework and clothing bridges    |
| `locales/*.json`       | All texts of the interface        |
| `web/audio`, `web/img` | Your music files and images       |

## ------FAQ------

**The selection screen does not open / endless black screen.** Check F8 and the server console. Most of the time another multicharacter is still running, or the framework is not set up as in STEP 3.

**The weather or time flickers in selection.** Make sure your weather sync starts before this resource. qb-weathersync, Renewed-Weathersync, cd\_easytime and vSync are paused automatically, others can be added in `environment.sync`.

**The game crashes when switching between characters.** You are close to the clothing YMT limit. Reduce streamed clothing packs.

**Starter items are not given.** Set `starterInventory` to your inventory. If yours is not in the list, add it in `hooks/server.lua`.

**Emote poses don't play on the selection screen.** Characters are shown on preview peds there, emote menus only animate the player. Use `dict` / `clip` or `scenario` poses in `config/poses.lua`.

**A character stands in a wall in my own scene.** Move the scene a little in `config/scenes.lua`. Height is fixed on its own, the x and y have to be on open ground.

**The camera is behind a wall in a duo location.** The camera sits further back for two characters. Set `camera = { distance = 0.8 }` for that location in `config/duo.lua`, or move the location to a more open spot.

**Lyrics are missing for a song.** Add plain lines or an `.lrc` file to that track in `config/music.lua`.

**Lyrics are a bit early or late.** Use the `-` / `+` buttons next to the lyrics button. For a track in `config/music.lua` you can set `offset` so it is right for everyone.

## INFOS

**Tebex :** [**https://bedoflexdev.tebex.io/package/7757048**](https://bedoflexdev.tebex.io/package/7757048)

**Discord :** [**https://bedoflexdev.net/discord**](https://bedoflexdev.net/discord)

<figure><img src="https://1782391057-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHE47mDCYcwHfrPASyBXt%2Fuploads%2FfzcMrRF0p2puvQZIfaCb%2FK%C4%B1rm%C4%B1z%C4%B1%20Tipografik%20Modern%20E%20Spor%20Oyun%20Logo%20(1).png?alt=media&amp;token=539027c9-3653-45d7-a315-ddb5b0221a57" alt=""><figcaption></figcaption></figure>
