# Developer API 2.0

Everything on [Developer API](/new/multicharacter/developer-api) still works. This page covers what 2.0 adds.

## Character status

Characters passed to exports, hooks and events also carry:

```lua
{
    status = 'active',          -- 'active' | 'dead' | 'trashed' | 'pending' | 'denied'
    dead = { at = 1761580000, reason = 'Shot during the bank job' },     -- or nil
    trash = { at = 1761580000, purgeAt = 1762184800 },                    -- or nil
    approval = { state = 'pending', at = 1761580000, reason = nil },      -- or nil
    nameRequest = { firstname = 'Jon', lastname = 'Carter', reason = '...', state = 'pending', note = nil, at = 0 }, -- or nil
    fields = { ... },           -- card fields from RegisterCardField
    scene = 'Mission Row PD',   -- label of the scene location, or nil
}
```

Only `active` characters that are not `disabled` can be played.

## Server exports

| Export | Returns | Description |
| --- | --- | --- |
| `GetCharacterStatus(charId)` | status, details | Status string and `{ dead, trash, approval }` |
| `SetCharacterDead(charId, reason \| false)` | boolean, reason? | CK a character (memorial card). `false` reverts it |
| `TrashCharacter(charId)` | boolean, reason? | Moves a character to the trash (permanent delete when the trash is off) |
| `RestoreCharacter(charId)` | boolean, reason? | Takes a character out of the trash |
| `ApproveCharacter(charId)` | boolean, reason? | Approves a pending or denied character |
| `DenyCharacter(charId, reason?)` | boolean, reason? | Denies a pending character |
| `RenameCharacter(charId, firstname, lastname)` | boolean, reason? | Renames a character, also while it is online |
| `ApproveNameRequest(charId)` / `DenyNameRequest(charId, note?)` | boolean, reason? | Handles a player's name change request |
| `GetPendingQueue()` | table[] | Pending approvals and name requests |
| `AddNote(target, text, author?, charId?)` | boolean | Staff note on an account |
| `GetNotes(target)` | table[] | Notes of an account |
| `AddAudit(target, charId?, action, details?)` | boolean | Writes your own entry into the account history |
| `GetAudit(target, limit?)` | table[] | Account history, newest first |
| `RegisterCardField(data)` / `RemoveCardField(id)` | boolean | Extra rows on the character card |
| `GetParty(src)` / `CreateParty(src)` / `JoinParty(src, code)` / `LeaveParty(src)` | | Party of a player |
| `GetSeason()` | string \| nil | Active season name |
| `OpenAdminPanel(src)` | boolean | Opens the admin panel for a player. No permission check, do your own |

`target` is a server id, a license, `discord:<id>` or `fivem:<id>`. The last two need the player to have joined once.
`AddSlots` and the other slot exports accept the same targets.

### RegisterCardField

```lua
exports['koja-multicharacter']:RegisterCardField({
    id = 'arrests',
    label = 'Arrests',
    icon = 'gavel',
    order = 10,
    badge = false,
    tone = nil,
    resolve = function(character, src)
        local count = MySQL.scalar.await('SELECT COUNT(*) FROM mdt_arrests WHERE charid = ?', { character.charId })
        if count == 0 then return nil end
        return count, 'last 30 days'
    end,
})
```

| Field | Description |
| --- | --- |
| `icon` | Lucide icon name, same list as `RegisterSpawn` |
| `order` | Lower = higher on the card |
| `badge` | `true` also shows the value as a tag on the slot list |
| `tone` | `'accent'`, `'danger'` or `'muted'` |
| `resolve` | Returns `value, sub?, { label?, icon?, tone? }?`. `nil` hides the field for that character |

`resolve` runs every time the character list loads, so keep it fast. Fields are removed when your resource stops.

## Hooks

| Hook | Arguments | Return |
| --- | --- | --- |
| `beforeTrash` | `src, character` | `false, reason` cancels |
| `afterRestore` | `src, character` | |
| `beforeNameRequest` | `src, character, { firstname, lastname, reason }` | `false, reason` cancels |
| `afterRename` | `charId, firstname, lastname` | |
| `beforePartyJoin` | `src, code, members` | `false, reason` cancels |
| `sceneLocation` | `src, character` | `{ label, ped = vec4(...), hour?, weather? }`. The first hook that returns a table wins |
| `sceneVehicle` | `src, character` | `{ model, props? }` forces a car, `false` hides it, `nil` keeps the default |
| `buildScene` | `src, character` | A table merged into `context.data` of client scene hooks |

```lua
exports['koja-multicharacter']:RegisterHook('sceneLocation', function(src, character)
    local house = MyHousing.GetMainHouse(character.charId)
    if house then
        return { label = house.label, ped = house.frontDoor, hour = 20 }
    end
end)

exports['koja-multicharacter']:RegisterHook('buildScene', function(src, character)
    local pet = MyPets.GetActive(character.charId)
    return pet and { pet = pet.model } or nil
end)
```

## Server events

| Event | Arguments |
| --- | --- |
| `koja-multicharacter:server:characterTrashed` | `charId` |
| `koja-multicharacter:server:characterRestored` | `charId` |
| `koja-multicharacter:server:characterDead` | `charId, reason` |
| `koja-multicharacter:server:characterRevived` | `charId` |
| `koja-multicharacter:server:characterSubmitted` | `src, character` |
| `koja-multicharacter:server:characterApproved` | `charId` |
| `koja-multicharacter:server:characterDenied` | `charId, reason` |
| `koja-multicharacter:server:nameRequested` | `src, character, request` |
| `koja-multicharacter:server:characterRenamed` | `charId, firstname, lastname` |
| `koja-multicharacter:server:partyChanged` | `code, members` |

## Client exports

| Export | Description |
| --- | --- |
| `RegisterSceneHook(fn)` / `RemoveSceneHook()` | Runs `fn(ped, context)` whenever a character settles in the selection scene. Return a function to clean up when the ped is removed |
| `GetSceneInfo()` | `{ slot, location, coords, season, ped, vehicle }` while the selection is open |
| `RegisterArrival(name, fn)` | Adds an arrival cutscene. Set `Config.Arrival.Mode = name` to use it |
| `IsArrivalPlaying()` / `SkipArrival()` | |
| `PlaySound(name)` | Plays a sound from `Config.Audio.Sounds` |
| `SetMusic(path \| nil)` | Crossfades the music to a file in the resource (`'sounds/x.mp3'`) or stops it |

Scene hook context: `{ slot, charId, ped, base = vec4, location, season, status, data }`.

```lua
exports['koja-multicharacter']:RegisterSceneHook(function(ped, context)
    if not context.data.pet then return end
    local model = joaat(context.data.pet)
    RequestModel(model)
    while not HasModelLoaded(model) do Wait(0) end
    local spot = GetOffsetFromEntityInWorldCoords(ped, 0.9, 0.4, -1.0)
    local pet = CreatePed(28, model, spot.x, spot.y, spot.z, context.base.w, false, true)
    FreezeEntityPosition(pet, true)
    return function() DeleteEntity(pet) end
end)
```

Arrival context: `{ coords, character, ped, sound(name), title(), sleep(ms), camera(vec3, fov), track(entity), skipped() }`.
`sleep` returns `false` once the player skips. Entities passed to `track` and cameras made with `camera` are removed when the cutscene ends, and the player is put on `coords` afterwards.
