# Developer API

Every export is called as `exports['koja-multicharacter']:Name(...)`.

## Character object

Hooks, events and exports pass characters in this shape:

```lua
{
    charId = 'char1:2a8f...',   -- ESX identifier or QB citizenid
    slot = 1,
    firstname = 'John',
    lastname = 'Carter',
    dateofbirth = '1994-05-12',
    age = 31,
    gender = 'male',           -- 'male' | 'female'
    nationality = 'American',
    height = 182,
    job = { name = 'police', label = 'LSPD', grade = 'Officer' },
    gang = nil,                -- QB: { name, label, grade }
    cash = 500, bank = 12000,
    playtime = 7200,           -- seconds
    lastPlayed = 1761580000, createdAt = 1761000000,
    disabled = false,
    backstory = '...',
}
```

## Server exports

| Export | Returns | Description |
| --- | --- | --- |
| `GetCharacter(src)` | character \| nil | The character the player is playing, nil while choosing |
| `GetCharacters(srcOrLicense)` | character[] | All characters of a player or license |
| `GetState(src)` | string | `connecting`, `selecting`, `loading`, `creating`, `spawning`, `playing` or `offline` |
| `IsInSelection(src)` | boolean | True while the player is in any multicharacter screen |
| `GetPlaytime(src)` | number | Playtime of the current character in seconds (stored + live) |
| `GetLicense(src)` | string | The license used as the account key |
| `GetSlots(srcOrLicense)` | number | Slot count (override, ACE tier or default) |
| `SetSlots(srcOrLicense, slots)` | boolean | Sets a slot override |
| `AddSlots(srcOrLicense, amount)` | boolean | Adds slots on top of the current count, for example after a Tebex purchase |
| `ResetSlots(srcOrLicense)` | boolean | Removes the override |
| `SetCharacterDisabled(charId, state)` | boolean | Locks or unlocks a character. A locked character online is sent back to the selection |
| `DeleteCharacter(charId)` | boolean | Deletes a character, also offline |
| `Logout(src, force?)` | boolean, reason? | Sends the player back to the selection. Without `force` the `beforeLogout` hooks can cancel it |
| `OpenSelection(src)` | boolean | Same as `Logout(src, true)` |
| `RegisterSpawn(data)` | boolean, reason? | Adds a spawn point, removed when your resource stops |
| `RemoveSpawn(id)` | boolean | Removes a spawn point |
| `GetSpawns()` | table[] | Registered spawn points |
| `RegisterHook(name, fn)` | id | Registers a hook, removed when your resource stops |
| `RemoveHook(id)` | boolean | Removes a hook |

### RegisterSpawn

```lua
exports['koja-multicharacter']:RegisterSpawn({
    id = 'vespucci_pd',
    label = 'Vespucci PD',
    description = 'Beach precinct',
    icon = 'shield',
    category = 'job',                -- last | public | job | gang | property | any string
    categoryLabel = 'Workplace',     -- optional
    coords = vec4(-1108.2, -845.1, 19.3, 128.0),
    jobs = { police = 2 },           -- job -> minimum grade, or a list { 'police', 'sheriff' }
    gangs = nil,                     -- same format, QB gangs
    cooldown = 300,                  -- seconds before this character can use it again
    hideLocked = false,              -- true hides it instead of greying it out
    newCharacters = false,           -- false: never offered to brand new characters
    canUse = function(src, character)
        return true                  -- or false, 'reason shown in the UI'
    end,
})
```

Icons the UI ships: `anchor`, `building`, `building-2`, `bus`, `car`, `castle`, `church`, `crosshair`, `factory`, `flame`, `fuel`, `gavel`, `home`, `hospital`, `hotel`, `house`, `landmark`, `map-pin`, `mountain`, `navigation`, `plane`, `shield`, `shield-alert`, `shopping-bag`, `ship`, `siren`, `skull`, `store`, `tent`, `train-front`, `trees`, `umbrella`, `warehouse`, `wrench`. Anything else shows `map-pin`.

## Hooks

Hooks run in registration order. A `before*` hook that returns `false, 'reason'` cancels the action and the reason is shown to the player. `after*` hooks cannot cancel.

| Hook | Arguments | Can cancel |
| --- | --- | --- |
| `beforeCreate` | `src, identity` | yes |
| `afterCreate` | `src, character` | no |
| `beforeSelect` | `src, character` | yes |
| `afterSelect` | `src, character` | no |
| `beforeDelete` | `src, character` | yes |
| `afterDelete` | `src, character` | no |
| `beforeSpawn` | `src, character, spawn, isNew` | yes. With koja-spawnselector it runs for every listed location and locks the rejected ones |
| `afterSpawn` | `src, character, spawn, isNew` | no |
| `beforeLogout` | `src, character` | yes, ignored when forced |
| `afterLogout` | `src, character` | no |
| `buildSpawns` | `src, character, isNew` | returns a list of spawn tables (same fields as `RegisterSpawn`) |

```lua
exports['koja-multicharacter']:RegisterHook('beforeCreate', function(src, identity)
    if identity.lastname:lower() == 'santa' then
        return false, 'Santa is not available this year.'
    end
end)

exports['koja-multicharacter']:RegisterHook('buildSpawns', function(src, character, isNew)
    local spawns = {}
    for _, house in ipairs(MyHousing.GetOwned(character.charId)) do
        spawns[#spawns + 1] = {
            id = 'house_' .. house.id,
            label = house.label,
            icon = 'house',
            category = 'property',
            coords = house.entrance,
        }
    end
    return spawns
end)

exports['koja-multicharacter']:RegisterHook('afterSpawn', function(src, character, spawn, isNew)
    if isNew then
        KojaLib.Server.addMoney(src, 2500, 'bank', 'starter bonus')
    end
end)
```

## Server events

| Event | Arguments |
| --- | --- |
| `koja-multicharacter:server:characterCreated` | `src, character` |
| `koja-multicharacter:server:characterSelected` | `src, character` |
| `koja-multicharacter:server:characterDeleted` | `src, character` |
| `koja-multicharacter:server:playerSpawned` | `src, character, spawn, isNew` |
| `koja-multicharacter:server:characterLogout` | `src, character` |

```lua
AddEventHandler('koja-multicharacter:server:playerSpawned', function(src, character, spawn, isNew)
    print(('%s %s spawned at %s'):format(character.firstname, character.lastname, spawn.label))
end)
```

## Client exports

| Export | Returns | Description |
| --- | --- | --- |
| `GetState()` | string | `idle`, `selection`, `loading`, `creating`, `spawning` or `playing` |
| `IsInSelection()` | boolean | True in any multicharacter screen |
| `IsPlaying()` | boolean | True once spawned |
| `GetCharacter()` | `{ firstname, lastname }` | Name of the current character |
| `CanRelog()` | boolean, reason? | Whether `/relog` would work right now |
| `Relog()` | boolean, reason? | Runs the same flow as `/relog` |
| `SetRelogBlocked(reason \| false)` | | Blocks switching characters for your resource until you pass `false` |
| `IsSpawnProtected()` | boolean | Spawn protection is active |
| `StopSpawnProtection()` | | Ends spawn protection now |

```lua
exports['koja-multicharacter']:SetRelogBlocked('You are handcuffed')

exports['koja-multicharacter']:SetRelogBlocked(false)
```

## Client events

| Event | Arguments |
| --- | --- |
| `koja-multicharacter:client:selectionOpened` | |
| `koja-multicharacter:client:spawned` | `character, isNew` |
| `koja-multicharacter:client:logout` | |

Use `selectionOpened` and `spawned` to hide and show your HUD.

## State bags

| Key | Where | Value |
| --- | --- | --- |
| `kojaInSelection` | `Player(src).state`, replicated | true while choosing |
| `kojaCharacter` | `Player(src).state`, replicated | `{ charId, slot, firstname, lastname, gender }` while playing |
| `kojaMulticharacterState` | `LocalPlayer.state`, client only | client state string |

```lua
local character = Player(source).state.kojaCharacter
```
