Koja Scripts

Developer API

Koja Spawn Selector API: import.lua, every client and server export, open options, the spawn object, providers, access checks, categories, events and recipes.

Developer API

Other resources can open the selector, add their own locations, lock locations with their own rules and react when a player spawns. Combat logging, death, jail and forced spawns have their own page: Anti combat log & restrictions. The editor, events, photos and stats: Admin tools.

Two ways to call it

Exports work from anywhere:

exports['koja-spawnselector']:Open({ canCancel = true })

import.lua gives you a SpawnSelector table with every export as a function, plus OpenAsync and SpawnAtAsync on the client:

fxmanifest.lua (your resource)
shared_script '@koja-spawnselector/import.lua'
SpawnSelector.Open({ canCancel = true })
local spawn = SpawnSelector.OpenAsync({ canCancel = true })

All exports at a glance

SideExportPage
ClientOpen, Close, IsOpen, RegisterSpawn, RemoveSpawn, SpawnAt, FlyThis page
Client (import.lua)SpawnSelector.OpenAsync, SpawnSelector.SpawnAtAsyncThis page
ServerOpen, Close, IsSelecting, RegisterSpawn, RegisterSpawns, RemoveSpawn, RegisterProvider, RemoveProvider, AddAccessCheck, RemoveAccessCheck, RegisterCategory, GetSpawns, GetLastLocation, SetLastLocationThis page
ServerMarkCombat, IsInCombat, GetDropRecord, ClearDropRecord, LockLastLocation, UnlockLastLocation, AddLastLocationRule, RemoveLastLocationRule, SetRestriction, ClearRestriction, AddRestrictionHandler, RemoveRestrictionHandlerAnti combat log
ServerAddEvent, RemoveEvent, SavePoint, DeletePoint, GetPoints, GetStatsAdmin tools

Everything another resource registers (spawns, providers, checks, rules, handlers, categories) is removed automatically when that resource stops.

Client

Open

local opened = exports['koja-spawnselector']:Open(options)

Returns false when the selector is already open, busy, or ClientEditable.CanOpen refused. Every option is optional:

OptionTypeWhat it does
isNewbooleanNew character: list Config.NewCharacter.Spawns instead of the normal list
canCancelbooleanAllow ESC and the exit button. Default Config.Open.CanCancel. Ignored while a restriction is active
categoriesstring[]Only these category ids
lastLocationbooleanfalse hides "Last location"
spawnsspawn[]Extra locations for this opening only (client side, see below)
includeServerbooleanfalse lists only spawns and the client registered ones
selectedstringPreselected spawn id
cinematicbooleanCamera flight on or off for this opening
titlestringReplaces the panel title
onSpawnfunction(spawn)Called after the player landed
onCancelfunction()Called whenever the selector closes without a spawn (ESC, Close, server Close)
Open after your own character creator
exports['koja-spawnselector']:Open({
    isNew = true,
    onSpawn = function(spawn)
        print(('spawned at %s'):format(spawn.label))
    end,
})

OpenAsync (import.lua)

CreateThread(function()
    local spawn = SpawnSelector.OpenAsync({ canCancel = true, categories = { 'property' } })
    if not spawn then return end -- cancelled
    print(spawn.id, spawn.coords.x, spawn.coords.y)
end)

Takes the same options as Open. Returns the spawn, or nil when it was cancelled or could not open.

Close, IsOpen

exports['koja-spawnselector']:Close()  -- calls onCancel
exports['koja-spawnselector']:IsOpen() -- true while open or while the camera flies

RegisterSpawn, RemoveSpawn

A location only this client sees, until you remove it or the resource restarts. Registering the same id again replaces it.

exports['koja-spawnselector']:RegisterSpawn({
    id = 'motel_room',
    label = 'Pink Cage Motel',
    description = 'Room 4',
    category = 'property',
    coords = vec4(312.3, -218.6, 54.2, 160.0),
})
exports['koja-spawnselector']:RemoveSpawn('motel_room')

SpawnAt, Fly, SpawnAtAsync

The same camera flight without the selector — after a jail release, a respawn or your own creator.

exports['koja-spawnselector']:SpawnAt(vec4(1846.4, 2585.9, 45.7, 270.0), {
    label = 'Bolingbroke',
    description = 'Released',
    cinematic = true,
    overlay = true,
    vehicle = 'bus',
    onLanded = function(coords) print('landed') end,
})
exports['koja-spawnselector']:Fly(vec4(-160.0, 6420.0, 31.9, 135.0), { label = 'Paleto Bay' })
OptionWhat it does
label, descriptionText of the letterbox caption. Default: the street name
cinematicCamera flight (true) or a plain fade. Default Config.Cinematic.Enabled
overlayfalse hides the caption
vehicleModel or { model, plate, props, fuel, keys }: land in the driver seat
onLandedfunction(coords), called after the player landed

SpawnAt returns true right away (false for invalid coords or while another spawn runs). Fly is SpawnAt with cinematic = true. With import.lua, SpawnSelector.SpawnAtAsync(coords, options) waits until the player landed.

Server

Open, Close, IsSelecting

exports['koja-spawnselector']:Open(source, { canCancel = true })
exports['koja-spawnselector']:Close(source)
exports['koja-spawnselector']:IsSelecting(source)

The options are the client ones without functions (they cannot cross the network). Listen to the events below instead.

RegisterSpawn, RegisterSpawns, RemoveSpawn

Locations for every player, with the same locks as Config.Spawns.

exports['koja-spawnselector']:RegisterSpawns({
    { id = 'lsfd', label = 'Davis Fire Station', category = 'job', coords = vec4(204.8, -1641.6, 29.8, 320.0), jobs = { fire = 0 }, onDuty = true },
    { id = 'vu', label = 'Vanilla Unicorn', category = 'job', coords = vec4(129.2, -1299.3, 29.2, 210.0), jobs = 'unicorn' },
})

RegisterSpawn returns true when the definition is valid, RegisterSpawns returns how many were added.

RegisterProvider, RemoveProvider

A provider returns the locations of one player and is called every time that player opens the selector. Use it for houses, apartments, garages, motels — anything a player owns. Built-in providers cover the common housing scripts (Config.Housing).

exports['koja-spawnselector']:RegisterProvider('my-housing', function(source, identifier)
    local houses = MySQL.query.await('SELECT id, label, x, y, z, h FROM houses WHERE owner = ?', { identifier })
    local list = {}
    for _, house in ipairs(houses or {}) do
        list[#list + 1] = {
            id = 'house:' .. house.id,
            label = house.label,
            category = 'property',
            coords = vec4(house.x, house.y, house.z, house.h),
            data = { house = house.id },
        }
    end
    return list
end)

identifier is the character identifier from koja-lib (ESX identifier, QBCore citizenid). Errors are caught and printed, so one broken provider does not break the screen.

Garage provider: spawn in your own car
exports['koja-spawnselector']:RegisterProvider('my-garage', function(source, identifier)
    local car = MySQL.single.await('SELECT plate, vehicle FROM owned_vehicles WHERE owner = ? AND stored = 1 LIMIT 1', { identifier })
    if not car then return {} end
    local props = json.decode(car.vehicle)
    return { {
        id = 'garage:' .. car.plate,
        label = 'Legion Square Garage',
        description = car.plate,
        category = 'property',
        coords = vec4(215.9, -809.8, 30.7, 250.0),
        vehicle = { model = props.model, plate = car.plate, props = props },
    } }
end)

AddAccessCheck, RemoveAccessCheck

Your own lock rule, run for every listed location after the config locks:

exports['koja-spawnselector']:AddAccessCheck('vip', function(source, spawn)
    if spawn.category == 'vip' and not Player(source).state.vip then
        return false, 'VIP only'
    end
    return true
end)

Return false, reason to lock the spot; the reason is shown as its label. Anything else allows it.

RegisterCategory

exports['koja-spawnselector']:RegisterCategory({ id = 'business', label = 'Business', icon = 'building', order = 35 })

order is compared with the order of Config.Categories.

GetSpawns

local list = exports['koja-spawnselector']:GetSpawns(source, { isNew = false })

The exact list this player would see, with locked and lockedLabel filled in.

GetLastLocation, SetLastLocation

local last = exports['koja-spawnselector']:GetLastLocation(source) -- or a character identifier
-- { coords = { x, y, z, w }, updated = 1793532461 } or nil

exports['koja-spawnselector']:SetLastLocation(source, vec4(1846.4, 2585.9, 45.7, 270.0))

The spawn object

onSpawn, OpenAsync and the events give you:

FieldDescription
id, label, description, category, icon, blip, imageFrom the definition
coords{ x, y, z, w }
vehicle{ model, plate, props, fuel, keys } when the spot spawns a vehicle
expiresAtEvents only: unix time the event ends
dataYour data table
originconfig, server, provider, last or client
updatedLast location only: unix time it was saved
street, zoneClient only: street and zone name at the spot

Events

EventSideArgumentsWhen
koja-spawnselector:client:openedClientoptionsThe map is shown
koja-spawnselector:client:spawnedClientspawn, isNewThe player landed
koja-spawnselector:client:closedClientoptionsClosed without a spawn
koja-spawnselector:client:combatLoggedClientpayloadA combat logger marker appeared near this player
koja-spawnselector:server:spawnedServersource, spawn, isNewThe player landed
koja-spawnselector:server:combatLogServeridentifier, recordA player left during combat
koja-spawnselector:server:pointsChangedServerid, point or nilAn editor location was saved or deleted
Server
AddEventHandler('koja-spawnselector:server:spawned', function(source, spawn, isNew)
    if spawn.origin == 'provider' and spawn.data and spawn.data.house then
        TriggerEvent('my-housing:server:enter', source, spawn.data.house)
    end
end)

On the server, spawn is the location the server listed and checked itself — not something the client sent — so its data is safe to act on. The only exception is origin == 'client': a location that came from client side RegisterSpawn / spawns, which the server never saw. Do not trust its data for ownership.

You can also open and close from the server with TriggerClientEvent('koja-spawnselector:client:open', source, options) and TriggerClientEvent('koja-spawnselector:client:close', source).