# 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](/new/spawn-selector/anti-combat-log). The editor, events, photos and stats: [Admin tools](/new/spawn-selector/admin-tools).

## Two ways to call it

**Exports** work from anywhere:

```lua
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:

```lua title="fxmanifest.lua (your resource)"
shared_script '@koja-spawnselector/import.lua'
```

```lua
SpawnSelector.Open({ canCancel = true })
local spawn = SpawnSelector.OpenAsync({ canCancel = true })
```

:::hint{type="info" title="Why the Async functions are only in import.lua"}
An export cannot wait inside another resource. Every export returns right away and reports back through a callback; the `Async` functions of `import.lua` wait for that callback in your own thread, so they are safe to call from any `CreateThread` or event handler.
:::

## All exports at a glance

| Side | Export | Page |
| --- | --- | --- |
| Client | `Open`, `Close`, `IsOpen`, `RegisterSpawn`, `RemoveSpawn`, `SpawnAt`, `Fly` | This page |
| Client (import.lua) | `SpawnSelector.OpenAsync`, `SpawnSelector.SpawnAtAsync` | This page |
| Server | `Open`, `Close`, `IsSelecting`, `RegisterSpawn`, `RegisterSpawns`, `RemoveSpawn`, `RegisterProvider`, `RemoveProvider`, `AddAccessCheck`, `RemoveAccessCheck`, `RegisterCategory`, `GetSpawns`, `GetLastLocation`, `SetLastLocation` | This page |
| Server | `MarkCombat`, `IsInCombat`, `GetDropRecord`, `ClearDropRecord`, `LockLastLocation`, `UnlockLastLocation`, `AddLastLocationRule`, `RemoveLastLocationRule`, `SetRestriction`, `ClearRestriction`, `AddRestrictionHandler`, `RemoveRestrictionHandler` | [Anti combat log](/new/spawn-selector/anti-combat-log) |
| Server | `AddEvent`, `RemoveEvent`, `SavePoint`, `DeletePoint`, `GetPoints`, `GetStats` | [Admin tools](/new/spawn-selector/admin-tools) |

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

## Client

### Open

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

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

| Option | Type | What it does |
| --- | --- | --- |
| `isNew` | boolean | New character: list `Config.NewCharacter.Spawns` instead of the normal list |
| `canCancel` | boolean | Allow ESC and the exit button. Default `Config.Open.CanCancel`. Ignored while a restriction is active |
| `categories` | string[] | Only these category ids |
| `lastLocation` | boolean | `false` hides "Last location" |
| `spawns` | spawn[] | Extra locations for this opening only (client side, see below) |
| `includeServer` | boolean | `false` lists only `spawns` and the client registered ones |
| `selected` | string | Preselected spawn id |
| `cinematic` | boolean | Camera flight on or off for this opening |
| `title` | string | Replaces the panel title |
| `onSpawn` | function(spawn) | Called after the player landed |
| `onCancel` | function() | Called whenever the selector closes without a spawn (ESC, `Close`, server `Close`) |

```lua title="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)

```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

```lua
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.

```lua
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')
```

:::hint{type="warning" title="Client locations are not checked by the server"}
Use `RegisterSpawn` / `spawns` on the client for things the player may see anyway. For ownership (houses, apartments), use a [server provider](#registerprovider-removeprovider): those are built and checked on the server.
:::

### SpawnAt, Fly, SpawnAtAsync

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

```lua
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' })
```

| Option | What it does |
| --- | --- |
| `label`, `description` | Text of the letterbox caption. Default: the street name |
| `cinematic` | Camera flight (`true`) or a plain fade. Default `Config.Cinematic.Enabled` |
| `overlay` | `false` hides the caption |
| `vehicle` | Model or `{ model, plate, props, fuel, keys }`: land in the driver seat |
| `onLanded` | function(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

```lua
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`.

```lua
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`).

```lua
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.

```lua title="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:

```lua
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

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

`order` is compared with the `order` of `Config.Categories`.

### GetSpawns

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

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

### GetLastLocation, SetLastLocation

```lua
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:

| Field | Description |
| --- | --- |
| `id`, `label`, `description`, `category`, `icon`, `blip`, `image` | From the definition |
| `coords` | `{ x, y, z, w }` |
| `vehicle` | `{ model, plate, props, fuel, keys }` when the spot spawns a vehicle |
| `expiresAt` | Events only: unix time the event ends |
| `data` | Your `data` table |
| `origin` | `config`, `server`, `provider`, `last` or `client` |
| `updated` | Last location only: unix time it was saved |
| `street`, `zone` | Client only: street and zone name at the spot |

## Events

| Event | Side | Arguments | When |
| --- | --- | --- | --- |
| `koja-spawnselector:client:opened` | Client | `options` | The map is shown |
| `koja-spawnselector:client:spawned` | Client | `spawn, isNew` | The player landed |
| `koja-spawnselector:client:closed` | Client | `options` | Closed without a spawn |
| `koja-spawnselector:client:combatLogged` | Client | `payload` | A combat logger marker appeared near this player |
| `koja-spawnselector:server:spawned` | Server | `source, spawn, isNew` | The player landed |
| `koja-spawnselector:server:combatLog` | Server | `identifier, record` | A player left during combat |
| `koja-spawnselector:server:pointsChanged` | Server | `id, point or nil` | An editor location was saved or deleted |

```lua title="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)`.
