Koja Scripts

Developer API 2.0

Koja Multicharacter 2.0 API: character statuses, CK, trash, approval and rename exports, card fields, scene and arrival hooks, notes, audit, party, events and client exports.

Developer API 2.0

Everything on Developer API still works. This page covers what 2.0 adds.

Character status

Characters passed to exports, hooks and events also carry:

{
    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

ExportReturnsDescription
GetCharacterStatus(charId)status, detailsStatus 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?)booleanStaff note on an account
GetNotes(target)table[]Notes of an account
AddAudit(target, charId?, action, details?)booleanWrites your own entry into the account history
GetAudit(target, limit?)table[]Account history, newest first
RegisterCardField(data) / RemoveCardField(id)booleanExtra rows on the character card
GetParty(src) / CreateParty(src) / JoinParty(src, code) / LeaveParty(src)Party of a player
GetSeason()string | nilActive season name
OpenAdminPanel(src)booleanOpens 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

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,
})
FieldDescription
iconLucide icon name, same list as RegisterSpawn
orderLower = higher on the card
badgetrue also shows the value as a tag on the slot list
tone'accent', 'danger' or 'muted'
resolveReturns 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

HookArgumentsReturn
beforeTrashsrc, characterfalse, reason cancels
afterRestoresrc, character
beforeNameRequestsrc, character, { firstname, lastname, reason }false, reason cancels
afterRenamecharId, firstname, lastname
beforePartyJoinsrc, code, membersfalse, reason cancels
sceneLocationsrc, character{ label, ped = vec4(...), hour?, weather? }. The first hook that returns a table wins
sceneVehiclesrc, character{ model, props? } forces a car, false hides it, nil keeps the default
buildScenesrc, characterA table merged into context.data of client scene hooks
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

EventArguments
koja-multicharacter:server:characterTrashedcharId
koja-multicharacter:server:characterRestoredcharId
koja-multicharacter:server:characterDeadcharId, reason
koja-multicharacter:server:characterRevivedcharId
koja-multicharacter:server:characterSubmittedsrc, character
koja-multicharacter:server:characterApprovedcharId
koja-multicharacter:server:characterDeniedcharId, reason
koja-multicharacter:server:nameRequestedsrc, character, request
koja-multicharacter:server:characterRenamedcharId, firstname, lastname
koja-multicharacter:server:partyChangedcode, members

Client exports

ExportDescription
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 }.

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.