Sal's Kewl Korner Documentation / Sal's Kewl Spawn Selector Discord

Sal's Kewl Spawn Selector — owner's manual

A drop-in replacement for qb-spawn with location previews, job-aware spawn points, per-spawn access rules and an in-game editor that lets an admin place spawns by flying to them. This manual covers installation, configuration, the editor and troubleshooting for server owners.

documents resource version 2.1.0

Overview

The spawn screen is the first thing every player sees, every session. Sal's Kewl Spawn Selector replaces qb-spawn with a menu that shows each location as a tile with its own preview image, a live camera fly-over of the spot under the cursor, and a list that changes per player based on their job, gang, licenses, items or citizen ID.

It registers itself as a qb-spawn provider, so resources that expect one keep working.

What it does

  • General and job spawns — everyone sees the general list; police see stations, EMS see hospitals, mechanics see garages.
  • Last location, with anti-abuse: hidden while a player is dead or in laststand, and optionally for a cooldown after combat, so logging out mid-fight is not a free escape.
  • Owned properties through loaf_housing.
  • Favorites and recents, starred by the player and stored in their metadata.
  • Search, for servers with a long list.
  • An in-game editor for admins — see below.
  • Discord roles instead of jobsComing soon — for servers that whitelist their departments in Discord rather than with framework jobs. In testing for 2.2.0; see Discord roles instead of jobs.

Server-authoritative by design

The client never sends coordinates. It sends an opaque spawn id; the server checks that id against the list that player was actually offered and returns the real coordinates itself. Restricted spawns are never sent to a client who cannot use them, so they cannot be discovered by reading network traffic, and a modified client cannot inject a spawn point, poison its own last location, or refill its own stats.

Requirements

Framework: qb-core, qbx_core or es_extended. Detection is automatic; Config.Framework = "auto" works out which you run. Force it with "qb" or "esx" if detection picks wrong.

There are no hard dependencies. Everything below is optional and detected at runtime, so a missing resource degrades gracefully instead of stopping the resource from starting.

ResourceWhat you lose without it
screenshot-basicThe live camera background behind the menu, and image capture in the spawn editor. Tiles still show their configured images.
loaf_housingThe Properties section. Everything else is unaffected.
illenium-appearanceAppearance loading before the menu opens. Falls back to qb-clothing if that is running.
oxmysqlUsed only by the illenium-appearance lookup.

ESX servers, read this. The framework bridge covers ESX for jobs, metadata, inventory, stats, revive and notifications, but three things are QB-only and will simply never match on ESX: restrict.gangs (ESX has no gang concept), restrict.licenses (ESX licenses live in a separate resource), and Config.LastLocation.SuppressWhenDead. Job, grade, citizen ID and item rules all work normally.

Installation

  1. Drop the folder into your resources/ directory as sals_kewlspawnselector.
  2. Add ensure sals_kewlspawnselector to server.cfg, after your framework and any housing or appearance resource.
  3. Open config.lua and set your spawn points — or start the server and place them in-game with the editor.
  4. Restart the server.

Replacing qb-spawn

Remove or comment out ensure qb-spawn in server.cfg and ensure this resource instead. The manifest declares provide 'qb-spawn', so anything that depends on qb-spawn being present is satisfied.

If your multichar or character-creation resource opens the spawn menu itself, point it at this event instead — see Integration.

Check it started cleanly

With Config.ValidateOnStart on (the default), the server console reports the framework it detected and then validates every spawn, naming any that are missing a label, carry invalid coordinates, or point at an image that is not in html/images/:

[sals_kewlspawnselector] Server loaded (v2.1.0, framework: qb).
[sals_kewlspawnselector] Config validation passed.

A missing image is a warning, not an error — the tile falls back to default.png.

Where spawns come from

There are two places spawns can live, and it is worth understanding which one your server is using before you start editing.

config.luaThe file you ship with. The defaults, and the only source until somebody saves in the in-game editor.
data/overrides.jsonWritten by the in-game editor. Layered on top of config.lua every time the resource starts.

The split exists so the editor can never destroy your file. config.lua is never rewritten — your comments, your formatting and your ordering all survive.

The two layer differently, and the difference matters:

  • Spawn lists — once an admin saves in the editor, data/overrides.json owns Config.SpawnLocations and Config.JobSpawns outright. Editing those blocks in config.lua afterward will appear to do nothing.
  • Settings — merged key by key, so anything you never touched in the editor still comes from config.lua.

It tells you when this bites. If you edit the spawn lists in config.lua after the editor has saved, the resource notices at start and says so in the server console, and the editor shows a banner. You will not be left wondering why the file you just edited did nothing.

Putting the file back in charge

Either one works:

  • Press Reset to config.lua in the editor, or
  • delete data/overrides.json and restart the resource.

Making in-game work permanent

Press Export as Lua in the editor. It generates a config.lua-shaped block; paste it over the matching blocks at the bottom of the file, then delete data/overrides.json.

If you would rather never deal with any of this, set Config.Editor.Enabled = false and config.lua is the only source, always.

In-game spawn editor

New in 2.1.0. An admin runs /spawnedit and gets a full editor over the game: add, rename, reorder, duplicate and delete spawns without touching config.lua or restarting anything.

Permission is checked server-side against Config.Editor.Permission, the same way the spawn command is. The client only ever asks; the server decides whether the editor opens at all.

The free-cam

Every spawn needs two positions — where the player lands, and where the preview camera sits — and both are miserable to guess by hand. Press Place with free-cam on any spawn and fly to the spot instead:

KeyAction
W A S DFly
Q / EUp / down
Shift / CtrlFaster / slower
GDrop the spawn point here, snapped to the ground
CSet the preview camera to this exact view
XCapture the tile image from this view
EnterBack to the editor

A live coordinate readout sits in the corner while you fly, and your character is put back exactly where they were standing when you exit.

Capturing tile images

X screenshots the current view, downscales it, and saves it into html/images/ under a filename derived from the spawn's name. This needs screenshot-basic running — the same optional resource the menu backgrounds use. Turn the whole feature off with Config.Editor.AllowImageCapture = false.

A freshly captured image cannot be served from disk until the resource restarts, because FiveM expands a manifest's file list once at start. Until then the picture travels inline with the spawn data so players see it immediately, and the resource drops the inline copy by itself on the next start once the file is being served properly. You do not have to do anything.

Access rules and per-spawn actions

The full access rule set is editable per spawn as plain comma-separated fields, so you can gate a spawn behind a job, a gang, an item or a license without writing Lua. Teleport & test drops you on the spawn point to check it before you commit.

Settings tab

With Config.Editor.AllowSettings on, the editor also exposes the runtime settings — menu theme and search, feature toggles, favorites, on-spawn stats and the last-location rules. Saving applies them on the server and pushes them to every connected player straight away.

Framework, locale, debug and command settings are deliberately not editable in-game. They are read once when the resource starts, so changing them at runtime would tell you something untrue. Those stay in config.lua.

Editor settings

Config.Editor = {
    Enabled = true,             -- Master switch for the editor
    Command = "spawnedit",      -- Command that opens it
    Permission = "god",         -- "god", "admin", "mod" or "all"
    AllowSettings = true,       -- Also allow editing settings, not just spawns
    AllowImageCapture = true,   -- Allow screenshotting tile images
    MaxSpawns = 200,            -- Safety cap on how many spawns can be saved
    ImageWidth = 640,           -- Captured images are downscaled to this width
    ImageQuality = 0.82,        -- JPEG quality for captured images (0.1 - 1.0)
    MaxImageSizeKB = 512        -- Reject a captured image larger than this
}

Do not set Permission = "all". That hands every player on your server the ability to rewrite your spawn configuration.

Everything the editor sends is re-validated on the server before it is stored — coordinates, labels, job lists, access rules, settings and images alike. If a second admin saves while you have the editor open, your save is refused rather than silently overwriting their work; reopen the editor to pick up their changes.

Spawn & job spawn config

Coordinates use the v4(x, y, z, heading) helper defined at the top of config.lua. cameraCoords is where the preview camera sits when a player hovers the tile; leave it out and the camera defaults to 30 m directly above the spawn.

General spawns

Shown to everyone, unless you add a restrict block.

Config.SpawnLocations = {
    {
        label = "Downtown Plaza",
        coords = v4(215.80, -810.30, 30.70, 338.70),
        image = "downtown_plaza.png",
        cameraCoords = v4(221.94, -821.05, 30.35, 338.70)
    },
}

Job spawns

Only shown to players whose job is in the jobs list. Always use the array form, even for one job.

Config.JobSpawns = {
    {
        jobs = { "police", "bcso", "lspd" },
        label = "Police HQ",
        coords = v4(635.01, 3.46, 82.74, 50.54),
        cameraCoords = v4(667.92, -27.50, 82.54, 50.54),
        image = "police_hq.png"
    },
}

By default the player's job label is prefixed to the tile title (Police — Police HQ). Turn that off with Config.UI.HideJobNameInSpawns = true.

No jobs on your server? Gating these on a Discord role instead is coming in 2.2.0 — see Discord roles instead of jobs.

Images

Images live in html/images/ and are named in the spawn's image field. A missing file falls back to default.png rather than showing a broken tile. Tiles render at roughly 200×120, so there is nothing to gain from very large source images — and every player downloads them on first join.

Access rules

Any spawn — general or job — can carry an optional restrict table. A player sees the spawn only if they pass every rule present. Rules are evaluated server-side, so a spawn a player cannot use is never sent to them.

{
    label = "VIP Lounge",
    coords = v4(x, y, z, h),
    image = "vip.png",
    restrict = {
        jobs       = { "police", "sheriff" }, -- job must be one of these
        minGrade   = 2,                       -- job grade must be >= this
        gangs      = { "ballas" },            -- gang must be one of these
        citizenids = { "ABC12345" },          -- citizenid must be one of these
        licenses   = { "weapon" },            -- must hold one of these licenses
        items      = { "vip_pass" },          -- must carry one of these items
    }
}

Rules combine with AND; the lists inside each rule are OR. The example above means a police or sheriff of grade 2 or higher, who is in the Ballas, whose citizen ID is ABC12345, who holds a weapon license and is carrying a VIP pass — which is probably nobody. Use one or two rules, not all six.

The jobs = { … } shorthand on a job spawn still works and is equivalent to restrict.jobs.

All six are editable in the in-game editor without writing Lua. See the ESX note for the two rules that do not apply there.

A seventh rule, discordRoles, is coming in 2.2.0 — see Discord roles instead of jobs.

Discord roles instead of jobsComing soon

This is not in the release this manual documents. Discord role support is built and in testing for 2.2.0; nothing described anywhere else in this manual depends on it, and everything else keeps working with framework jobs exactly as it does now. This section is here so you can see what is coming and plan for it — the version pill at the top of the page changes to 2.2.0 when it ships, and this badge comes off.

Plenty of servers whitelist their departments in Discord and never set up qb jobs. In 2.2.0 any spawn can be gated on a Discord role exactly the way it can be gated on a job — including the whole Job Locations section, which is then named after the role rather than the player's job.

Roles, jobs, or both. None of it is required: leave Config.Discord.Enabled = false and the resource behaves exactly as it does today.

Whose bot: ours or yours

Reading a Discord role needs a bot somewhere. You can use ours and skip almost all of the setup, or run your own and route nothing through us. Both ship, and neither is a fallback for the other.

We do not put our bot's token in the resource, and that is deliberate. It would be one token shared by every customer, in a file every customer can read — extracted once, it would read every Discord server that ever added the bot. So when you use our bot, the lookup happens on our server, where the token already is.

Our bot — two steps

  1. Add our bot to your Discord with the invite link in your customer portal. It asks for zero permissions: reading a member's roles needs membership and nothing else, so there is nothing for you to grant.
  2. Put your link key in server.cfg:
set sals_spawn_key "your-link-key"

That is the whole setup — no developer portal, no bot token, nothing to keep secret but the key itself. Already running Sal's Kewl Dispatch? The key you have works for this too.

What we see: the Discord user ID of a player picking a spawn, and which of your roles they hold. Nothing else, and nothing about Discord ever reaches a game client.

Your own bot

Nothing leaves your server except a call to Discord.

  1. At discord.com/developers/applications, create an application, add a Bot, and copy the bot token.
  2. Invite the bot to your Discord. It needs no permissions beyond being a member — it only reads member roles.
  3. Turn on Developer Mode in Discord (Settings → Advanced). Right-click your server → Copy Server ID, and right-click each role → Copy Role ID.

Put the bot token in server.cfg, not in config.lua. config.lua is the file people zip up and send to each other when they ask for help with a setup; a leaked bot token has to be revoked and replaced everywhere. The same goes for your link key.

set sals_spawn_discord_token "your-bot-token"
set sals_spawn_discord_guild "your-server-id"

The config either way

Config.Discord = {
    Enabled = true,
    Source  = "auto",   -- "auto", "hosted", "api", "badger" or "custom"
    CacheSeconds = 300, -- how long a player's roles are remembered
    FailOpen = false,   -- lookup unreachable: false hides role-gated spawns

    -- Friendly names for your role ids. Use the name anywhere a role is asked for.
    Roles = {
        police = "123456789012345678",
        fire   = "123456789012345679",
        ems    = { id = "123456789012345680", label = "EMS" }
    }
}

"auto" picks your link key if you set one, then Badger_Discord_API if it is running, then your own bot token, then CustomExport. Switching between our bot and yours is two lines in server.cfg — your spawn config does not change, because a role ID means the same thing whoever reads it.

Any other role resource works through Config.Discord.CustomExport, which is called as exports[resource][method](source) and must return a list of role ids.

Using roles on a spawn

Config.JobSpawns = {
    {
        discordRoles = { "police" },   -- no jobs line at all
        label = "SASP HQ",
        coords = v4(635.01, 3.46, 82.74, 50.54),
        image = "nj_sasp.png"
    },
    {
        jobs = { "ambulance" },        -- both: the job OR the role gets in
        discordRoles = { "ems" },
        label = "Pillbox",
        coords = v4(295.00, -1446.00, 29.00, 270.00)
    },
}

General spawns take the same rule inside restrict:

{
    label = "VIP Lounge",
    coords = v4(x, y, z, h),
    restrict = { discordRoles = { "vip" } }
}

Both are editable in the in-game editor. The Discord fields appear on job spawns and in the access rules block only while Config.Discord.Enabled is on, so nobody fills in a rule that could never match.

Settings

SettingDefaultWhat it does
Config.Discord.EnabledfalseMaster switch. Off means jobs only, exactly as before.
Config.Discord.Source"auto""auto", "hosted" (our bot), "api" (your own bot), "badger" or "custom".
Config.Discord.Link{}For our bot: Url, TenantId and Key. Set the key in server.cfg as sals_spawn_key.
Config.Discord.BotToken""For your own bot. Better set in server.cfg as sals_spawn_discord_token.
Config.Discord.GuildId""Your Discord server id. Or sals_spawn_discord_guild in server.cfg.
Config.Discord.CustomExport{}{ resource = "…", method = "…" } for a role resource other than Badger.
Config.Discord.CacheSeconds300How long a player's roles are remembered. Lower = role changes land sooner; higher = fewer calls to Discord. A reconnect always re-checks.
Config.Discord.FailOpenfalseWhat happens when Discord cannot be reached. false hides role-gated spawns; true shows them to everyone.
Config.Discord.Roles{}Friendly name → role id. The name doubles as the tile prefix unless you give it a label.

What it does not do

Nothing about Discord reaches the client: the token, the guild id and the role ids stay server-side, and a player is only ever sent spawns they have already passed. Roles are looked up while the player is still loading in, so the spawn menu does not wait on Discord. A player who joined without Discord running simply holds no roles, and still sees everything that is not role-gated.

This does not grant permissions anywhere else. /spawnedit and the test-spawn command are still gated on your framework's admin groups (Config.Editor.Permission), not on Discord.

When it does not work

SymptomWhat to check
Discord-gated spawns do not appear The server console names the problem at start: a role that is not in Config.Discord.Roles, a missing bot token or guild id, or a bot that is not in the guild. With Config.Debug on, every player's resolved roles are logged. Remember roles are cached for Config.Discord.CacheSeconds — a role you just granted takes that long to land, or reconnect to force it.
Discord role lookup failed … Printed with the reason. A rejected token means the bot was removed or the token was regenerated; rate limited means CacheSeconds is too low for your player count. While lookups are failing, Config.Discord.FailOpen decides whether role-gated spawns hide (the default) or show.

For other resources

The same lookup is exported, so a job center or a whitelisted vehicle shop does not need its own:

local isCop  = exports['sals_kewlspawnselector']:HasDiscordRole(source, 'police')
local roleIds = exports['sals_kewlspawnselector']:GetDiscordRoleIds(source)

Settings reference

Everything below lives in config.lua. The groups marked editor can also be changed in-game on the editor's Settings tab.

General

SettingDefaultWhat it does
Config.DebugfalseVerbose client and server logging. Ships off; turn it on only while diagnosing something.
Config.Locale"en"Active language file in locales/.
Config.ValidateOnStarttrueCheck spawns and images at start and report problems.
Config.Framework"auto""auto", "qb" or "esx".

Menu — editor

SettingDefaultWhat it does
UI.Theme"dark""dark" or "light".
UI.showCancelButtonfalseLet a player back out of the normal spawn flow.
UI.HideJobNameInSpawnsfalseDrop the job label prefix from job spawn titles.
UI.EnableSearchtrueShow the search box.
UI.PreviewSettleTime250Milliseconds to let the camera settle before the background screenshot.

Features — editor

SettingDefaultWhat it does
Features.EnableHousingtrueShow owned properties (needs loaf_housing).
Features.UseIlleniumAppearancetrueLoad appearance via illenium-appearance.
Features.EnableDynamicBackgroundstrueLive screenshot behind the menu (needs screenshot-basic).

Favorites & recents — editor

SettingDefaultWhat it does
Favorites.EnabledtrueLet players star spawns and see a Recent row.
Favorites.MaxRecents4How many recent spawns to remember.

On spawn — editor

Applied server-side from config, never from anything the client sends.

SettingDefaultWhat it does
Spawn.SetStatstrueSet hunger and thirst on spawn.
Spawn.Food / Spawn.Water100The values used when SetStats is on.
Spawn.SetHealthtrueRestore health on spawn.
Spawn.SetArmor / ArmorValuefalse / 0Give armor on spawn.
Spawn.Invincible.EnabledtrueBrief invincibility so a player cannot die the instant they load in.
Spawn.Invincible.Duration1000How long, in milliseconds.

Last location — editor

SettingDefaultWhat it does
LastLocation.EnabledtrueOffer the player's last saved position.
LastLocation.SuppressWhenDeadtrueHide it while they are dead or in laststand. QB only.
LastLocation.TrackCombatfalseWatch for combat so the option can be withheld afterward.
LastLocation.CombatCooldownSeconds300How long to withhold it after combat. Needs TrackCombat.

TrackCombat is the setting that stops players from logging out in a fight and dropping straight back onto the same spot. It is off by default because it runs a lightweight client-side check; turn it on if combat logging is a problem on your server.

Commands

Both are permission-gated, and both check permission on the server.

CommandDoesDefault permission
/sals_testspawnOpens the spawn selector on demand. Name, permission and cancel-button behavior are set in Config.Command. god
/spawneditOpens the spawn editor. Set in Config.Editor.god

Permission levels are "god", "admin", "mod" and "all". On QB these map to the framework's own permission system; on ESX they map to ACE principals. Set enabled / Enabled to false to remove either command entirely.

Integration

Opening the selector from another resource

The normal spawn flow — multichar, character creation, respawn — is driven by one client event:

-- Server-side, for a specific player:
TriggerClientEvent('sals_kewlspawnselector:openSelector', playerId)

-- Client-side, e.g. from your own multichar handler:
TriggerEvent('sals_kewlspawnselector:openSelector')

When a player finishes spawning, the resource fires sals_kewlspawnselector:spawned on the client for your scripts to listen for.

Customization files

Two files exist to be edited and are kept separate from the rest so they survive updates:

client_customize.luaAppearance loading and the housing lookup. Replace CustomFunctions.GetPlayerHouses to support a housing resource other than loaf_housing — it returns a list of { propertyId, label, coords, image, cameraCoords }.
server_customize.luaCanOpenMenu(src) — return false to block the menu for a player. OnPlayerSpawned(src, key) — runs after a spawn is committed.

Translations

No user-facing text is hardcoded. locales/en.lua is the baseline; copy it to locales/<code>.lua, translate the values, and set Config.Locale. Missing keys fall back to English, so a partial translation is safe to ship.

Troubleshooting

SymptomWhat to check
Edits to config.lua spawns do nothing The in-game editor has saved, so data/overrides.json owns the spawn lists. The server console says so at start. See Where spawns come from.
Players fall through the map The z in that spawn's coordinates is below the floor. Stand on the spot and use Use my position in the editor, or G in the free-cam, which snaps to ground level.
Black screen, no menu Check the server console for a config validation failure, and that ensure sals_kewlspawnselector comes after your framework in server.cfg. If another resource also handles spawning, they will fight — qb-spawn in particular must not be running.
Job spawns do not appear Job names in jobs = { … } must match your framework's internal job names exactly, not the display labels. Turn on Config.Debug to see the job the server read for that player.
Properties do not appear loaf_housing must be started and Config.Features.EnableHousing on. Another housing resource needs CustomFunctions.GetPlayerHouses adapted — see Integration.
Tiles show the default image The named file is not in html/images/. Start-up validation names every spawn this affects. Filenames are case-sensitive on Linux servers.
No live background behind the menu screenshot-basic is not running, or Config.Features.EnableDynamicBackgrounds is off.
/spawnedit does nothing Permission is checked server-side — you are not in the group named by Config.Editor.Permission. The editor also refuses to open while the normal spawn menu is up, since the two would fight over the camera.
A captured image will not save The resource folder must be writable by the server process. The editor reports the failure on screen and the server console names the file it could not write.

When something is not behaving, Config.Debug = true logs the spawn flow on both sides. Turn it back off afterward — it is noisy.

Support

Support, release news and coupon codes are on Discord. Purchases are handled by Tebex, who own the checkout, billing support and refunds.

The resource ships its own copy of this manual at docs/help.html inside the download, including the full changelog.