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.
| Resource | What you lose without it |
|---|---|
screenshot-basic | The live camera background behind the menu, and image capture in the spawn editor. Tiles still show their configured images. |
loaf_housing | The Properties section. Everything else is unaffected. |
illenium-appearance | Appearance loading before the menu opens. Falls back to qb-clothing if that is running. |
oxmysql | Used 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
- Drop the folder into your
resources/directory assals_kewlspawnselector. - Add
ensure sals_kewlspawnselectortoserver.cfg, after your framework and any housing or appearance resource. - Open
config.luaand set your spawn points — or start the server and place them in-game with the editor. - 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.lua | The file you ship with. The defaults, and the only source until somebody saves in the in-game editor. |
|---|---|
data/overrides.json | Written 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.jsonownsConfig.SpawnLocationsandConfig.JobSpawnsoutright. Editing those blocks inconfig.luaafterward 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.jsonand 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:
| Key | Action |
|---|---|
| W A S D | Fly |
| Q / E | Up / down |
| Shift / Ctrl | Faster / slower |
| G | Drop the spawn point here, snapped to the ground |
| C | Set the preview camera to this exact view |
| X | Capture the tile image from this view |
| Enter | Back 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
- 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.
- 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.
- At discord.com/developers/applications, create an application, add a Bot, and copy the bot token.
- Invite the bot to your Discord. It needs no permissions beyond being a member — it only reads member roles.
- 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
| Setting | Default | What it does |
|---|---|---|
Config.Discord.Enabled | false | Master 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.CacheSeconds | 300 | How long a player's roles are remembered. Lower = role changes land sooner; higher = fewer calls to Discord. A reconnect always re-checks. |
Config.Discord.FailOpen | false | What 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
| Symptom | What 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
| Setting | Default | What it does |
|---|---|---|
Config.Debug | false | Verbose client and server logging. Ships off; turn it on only while diagnosing something. |
Config.Locale | "en" | Active language file in locales/. |
Config.ValidateOnStart | true | Check spawns and images at start and report problems. |
Config.Framework | "auto" | "auto", "qb" or "esx". |
Menu — editor
| Setting | Default | What it does |
|---|---|---|
UI.Theme | "dark" | "dark" or "light". |
UI.showCancelButton | false | Let a player back out of the normal spawn flow. |
UI.HideJobNameInSpawns | false | Drop the job label prefix from job spawn titles. |
UI.EnableSearch | true | Show the search box. |
UI.PreviewSettleTime | 250 | Milliseconds to let the camera settle before the background screenshot. |
Features — editor
| Setting | Default | What it does |
|---|---|---|
Features.EnableHousing | true | Show owned properties (needs loaf_housing). |
Features.UseIlleniumAppearance | true | Load appearance via illenium-appearance. |
Features.EnableDynamicBackgrounds | true | Live screenshot behind the menu (needs screenshot-basic). |
Favorites & recents — editor
| Setting | Default | What it does |
|---|---|---|
Favorites.Enabled | true | Let players star spawns and see a Recent row. |
Favorites.MaxRecents | 4 | How many recent spawns to remember. |
On spawn — editor
Applied server-side from config, never from anything the client sends.
| Setting | Default | What it does |
|---|---|---|
Spawn.SetStats | true | Set hunger and thirst on spawn. |
Spawn.Food / Spawn.Water | 100 | The values used when SetStats is on. |
Spawn.SetHealth | true | Restore health on spawn. |
Spawn.SetArmor / ArmorValue | false / 0 | Give armor on spawn. |
Spawn.Invincible.Enabled | true | Brief invincibility so a player cannot die the instant they load in. |
Spawn.Invincible.Duration | 1000 | How long, in milliseconds. |
Last location — editor
| Setting | Default | What it does |
|---|---|---|
LastLocation.Enabled | true | Offer the player's last saved position. |
LastLocation.SuppressWhenDead | true | Hide it while they are dead or in laststand. QB only. |
LastLocation.TrackCombat | false | Watch for combat so the option can be withheld afterward. |
LastLocation.CombatCooldownSeconds | 300 | How 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.
| Command | Does | Default permission |
|---|---|---|
/sals_testspawn | Opens the spawn selector on demand. Name,
permission and cancel-button behavior are set in Config.Command. |
god |
/spawnedit | Opens 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.lua | Appearance 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.lua | CanOpenMenu(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
| Symptom | What 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.