# RSG Multicharacter V2

A fully custom, React-based multicharacter selection system for RedM (RSG Framework). Features cinematic character previews with walking animations, horse mounts, per-slot camera angles, and a Tebex-integrated slot unlock system.

***

### Features

* **React-based NUI** - Modern, responsive character selection UI built with React + TypeScript + Tailwind CSS
* **Cinematic Character Previews** - Each character slot has unique world coordinates, walking paths, idle animations, and camera angles
* **Horse Mount Support** - Characters can spawn mounted on horses during the selection screen
* **Full Skin Loading** - Characters render with their saved appearance (skin + clothes) via `rsg-appearance`
* **Slot Lock/Unlock System** - Lock character slots behind Tebex purchases with transaction ID verification
* **Multi-SQL Support** - Works with oxmysql, ghmattimysql, and mysql-async out of the box
* **Starter Items & Horse** - New characters automatically receive starter items and an optional starter horse
* **Logout & Re-select** - Players can log out and return to character selection without reconnecting
* **Third-Party Integration** - Other resources can trigger character selection via event

***

### Dependencies

| Resource                                   | Required     |
| ------------------------------------------ | ------------ |
| `rsg-core`                                 | Yes          |
| `rsg-appearance`                           | Yes          |
| `rsg-spawn`                                | Yes          |
| `weathersync`                              | Yes          |
| `oxmysql` / `ghmattimysql` / `mysql-async` | One of these |

***

### Installation

1. Place `cas-multicharacter` in your server's `resources` folder
2. Add `ensure cas-multicharacter` to your `server.cfg` (after `rsg-core` and `rsg-appearance`)
3. Import the SQL file:

```sql
-- Run tebexintegratersg.sql in your database
-- This creates the required tables: cas_slots, cas_multichartebexids
```

4. If replacing the default `rsg-multicharacter`, ensure it is stopped:

```cfg
# server.cfg
#ensure rsg-multicharacter
ensure cas-multicharacter
```

***

### Configuration

#### Server Config (`config/server_config.lua`)

```lua
Config = {}

-- Starter horse given to new characters
Config.StarterHorse = true                              -- Enable/disable starter horse
Config.StarterHorseModel = 'a_c_horse_mp_mangy_backup'  -- Horse model hash
Config.StarterHorseStable = 'valentine'                  -- Starting stable location
Config.StarterHorseName = 'Starter Horse'                -- Default horse name
```

#### Client Config (`config/client_config.lua`)

```lua
Config = {}

Config.TebexLink = "https://your-store.tebex.io"        -- Your Tebex store URL
Config.discordLink = "https://discord.gg/your-server"    -- Your Discord invite link

-- Skin loading function (uses rsg-appearance export)
SkinFunction = function(skinData, ped, clothesData)
    exports['rsg-appearance']:ApplySkinMultiChar(skinData, ped, clothesData)
end
```

#### Character Slots (`config/shared_config.lua`)

Each character slot is defined in `Config.Characters`. You can add or remove slots as needed.

```lua
Config.Characters = {
    [1] = {
        ped = nil,
        skin = {
            clothesData = {},
            skinData = {},
            sex = nil,
        },
        camSettings = {
            cam = nil,
            fov = 20.0,           -- Camera field of view
            offsetX = 8.0,        -- Camera X offset from ped
            offsetY = 2.5,        -- Camera Y offset from ped
            offsetZ = 1,          -- Camera Z offset from ped
        },
        randomPeds = {             -- Random ped models for empty slots
            "cs_unidusterjail_01",
            "cs_mp_cripps",
            "cs_valprostitute_02",
        },
        scenarioData = {
            startCoord = vector4(-799.465, -1369.105, 43.540, 4.599),  -- Ped spawn location
            walkTo = vector4(-802.178, -1253.551, 43.457, 351.132),    -- Walk destination
            horseSettings = {      -- Optional: horse mount
                horse = nil,
                horseHash = `A_C_HORSE_AMERICANSTANDARDBRED_BLACK`,
                horseCoord = vector4(-799.946, -1365.228, 42.558, 0.541),
            },
            -- Optional: idle animation after reaching walkTo
            -- animation = {
            --     dict = "amb_rest_lean@world_human_lean@wall@right@male_b@idle_a",
            --     name = "idle_c",
            --     flag = 17,
            -- },
        },
        reactData = {
            locked = false,        -- true = requires Tebex unlock
            empty = true,
            firstName = nil,
            lastName = nil,
            lastLogin = nil,
            isDead = false,
            id = nil,
            jobLabel = nil,
        }
    },
    -- [2] = { ... },
    -- [3] = { ... },
}
```

**Slot Properties**

| Property                     | Type      | Description                                           |
| ---------------------------- | --------- | ----------------------------------------------------- |
| `camSettings.fov`            | number    | Camera zoom level (lower = more zoomed in)            |
| `camSettings.offsetX/Y/Z`    | number    | Camera position offset relative to the ped            |
| `scenarioData.startCoord`    | vector4   | Where the ped spawns (x, y, z, heading)               |
| `scenarioData.walkTo`        | vector4   | Where the ped walks to after spawning                 |
| `scenarioData.horseSettings` | table/nil | Optional horse mount configuration                    |
| `scenarioData.animation`     | table/nil | Optional idle animation (dict, name, flag)            |
| `reactData.locked`           | boolean   | Whether this slot requires a Tebex purchase to unlock |
| `randomPeds`                 | table     | List of ped models to use for empty/locked slots      |

***

### Tebex Slot Unlock System

The resource includes a Tebex integration for selling additional character slots.

#### How It Works

1. Admin creates a transaction ID via RCON/console:

```
purchaseslot <tebex_transaction_id>
```

2. Player enters the transaction ID in the NUI to unlock a locked slot
3. The system verifies the ID hasn't been used, marks it as used, and unlocks the slot for that player's license

#### Database Tables

```sql
-- Stores Tebex transaction IDs
CREATE TABLE IF NOT EXISTS `cas_multichartebexids` (
    `id` INT AUTO_INCREMENT PRIMARY KEY,
    `tebexId` VARCHAR(255) NOT NULL,
    `used` TINYINT(1) DEFAULT 0
);

-- Stores which slots each player has unlocked
CREATE TABLE IF NOT EXISTS `cas_slots` (
    `id` INT AUTO_INCREMENT PRIMARY KEY,
    `license` VARCHAR(255) NOT NULL,
    `unlockedSlots` LONGTEXT DEFAULT '[]'
);
```

***

### Events

#### Client Events

| Event                                   | Description                                                       |
| --------------------------------------- | ----------------------------------------------------------------- |
| `cas-multicharacter:LoadFromThirdParty` | Opens the character selection screen (for use by other resources) |
| `cas-multicharacter:client:logout`      | Triggers client-side logout and returns to character select       |

#### Server Events

| Event                              | Description                                                |
| ---------------------------------- | ---------------------------------------------------------- |
| `cas-multicharacter:server:logout` | Logs out the player server-side and triggers client logout |
| `cas-multicharacter:LoadCharacter` | Loads a selected character                                 |
| `cas-multicharacter:CreateNewChar` | Creates a new character with provided data                 |

#### Usage Example - Triggering Character Select from Another Resource

```lua
-- Client-side
TriggerEvent("cas-multicharacter:LoadFromThirdParty")
```

***

### NUI Callbacks

| Callback                            | Direction     | Description                                  |
| ----------------------------------- | ------------- | -------------------------------------------- |
| `onCharacterChange`                 | NUI -> Client | Player scrolls to a different character slot |
| `characterActions`                  | NUI -> Client | Player clicks Play or Delete on a character  |
| `createNewChar`                     | NUI -> Client | Player submits new character creation form   |
| `disconnect`                        | NUI -> Client | Player clicks disconnect button              |
| `hideFrame`                         | NUI -> Client | NUI requests to hide itself                  |
| `mbl:checkTransactionIdIsAvailable` | NUI -> Client | Verifies a Tebex transaction ID              |

***

### Server Callbacks

| Callback                                  | Description                                                            |
| ----------------------------------------- | ---------------------------------------------------------------------- |
| `mbl:fetchCharacters`                     | Fetches all character data for a player (skin, clothes, job, metadata) |
| `mbl:server:DeleteChar`                   | Deletes a character and its skin data                                  |
| `mbl:server:transactionCheck`             | Validates and redeems a Tebex transaction ID                           |
| `rsg-multicharacter:server:getAppearance` | Returns skin/clothes data for a citizenid (used by rsg-appearance)     |

***

### Commands

| Command             | Permission   | Description                                         |
| ------------------- | ------------ | --------------------------------------------------- |
| `logout`            | Everyone     | Returns to character selection screen               |
| `purchaseslot <id>` | Console/RCON | Registers a Tebex transaction ID for slot unlocking |

***

### Web UI Development

The NUI is built with React + TypeScript + Vite + Tailwind CSS.

#### Project Structure

```
web/
  src/
    components/
      App.tsx              -- Main application component
    hooks/
      useNuiEvent.ts       -- Hook for listening to NUI messages
      useCharacters.ts     -- Character data hook
      currentCharIndex.ts  -- Current selected character index
    slices/
      charactersSlice.ts   -- Character state management
      configSlice.ts       -- Config state
      localeSlice.ts       -- Localization state
    providers/
      VisibilityProvider.tsx -- Controls NUI visibility
    utils/
      fetchNui.ts          -- Fetch wrapper for NUI callbacks
      debugData.ts         -- Debug data for browser development
    store.ts               -- Redux/Zustand store
  build/                   -- Production build (served to game)
```

#### Building the UI

```bash
cd web
npm install
npm run build
```

The build output goes to `web/build/` which is served by the fxmanifest.

***

### Troubleshooting

#### Characters appear invisible or naked

* Ensure `rsg-appearance` is running and the `ApplySkinMultiChar` export is available
* Check that `playerskins` table has valid skin data for the character
* The resource includes a `baseModel()` fallback that applies basic body parts when no skin data exists

#### Character selection doesn't open on join

* Verify the resource is ensured after `rsg-core` in `server.cfg`
* Check that `weathersync` is running (required for time sync)
* Look for errors in the server console during the loading phase

#### NUI appears frozen or unresponsive

* All NUI callbacks must call `cb()` - this has been fixed in V2
* Check browser console (F8) for JavaScript errors

#### Slot unlock not working

* Verify the transaction ID was created via `purchaseslot` command
* Check `cas_multichartebexids` table to ensure the ID exists and `used = 0`
* Ensure `cas_slots` table exists in your database

#### Logout command not working

* The `logout` command now properly calls `RSGCore.Player.Logout()` on the server before returning to character select
* Ensure no other resource is overriding the `logout` command

***

### License

This resource is provided as-is. Modify and distribute according to your server's needs.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://code-after-sex.gitbook.io/script-documentation/about-paid-scripts/redm-script-documentation/rsg-multicharacter-v2.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
