Overview
The CAD dispatches, and the house goes out.
The lights come on, because that is what wakes people. A soft signal starts to rise. Six seconds in the tones drop, the bay doors run up, and the whistle on the roof winds from a low note to a high one and holds it. Then a voice reads the run — Station 1, structure fire, 2400 block Alta Street, Engine 1, Ladder 1. Time out 0347 — and reads it again once the tones have stopped ringing. The television on the dayroom wall pulls that call to the top and says ALERT. Somebody in the kitchen presses the reset at the watch desk and the noise stops, and dispatch can see who it was and how long they took.
That is what this resource is, and every single thing in that paragraph is timing. Nothing happens at the same instant as anything else. A system that fires eight events at once does not feel like a firehouse, it feels like a light switch — so the center of this resource is a small declarative scheduler that you write your own sequences against, and the paragraph above is one of the seven that ship.
Three things make it different from the obvious way of building this:
- Nothing is streamed and no audio file ships. Every tone, ramp, whistle and bell is a recipe — a description of a sound — synthesized at the moment it is needed. No download, no streamed asset, and no audio licensing question, ever. A board is a rectangle in the world rather than a prop for the same reason: it works in your station interior whoever made it and whenever they made it, including one released after this resource and a bare wall in a station with no television in it at all.
- Everything about your fire department is created in game. Your houses, their apparatus, the footprint of each building, and where every light, speaker, bay door and whistle is. There is no configuration file we could ship that would be right for your station, because we have never seen it.
- You ask for a house, never for a device. No caller anywhere — not a CAD, not a web console, not another resource, not a player — can name a door, a doorsystem hash or a lock id. See Setting a station off for why that is the most important sentence in this manual.
It ships showing invented calls, so you can see the whole thing working before you have wired it to anything.
What ships
All of it. This manual documents behavior that is in the download you have.
| Alerting | The sequence engine, seven sequences, and rule matching per call. Rotating lights and LED strip. Bay doors with five drivers. The whistle, the house speakers, two-tone paging, the soft-start ramp, and the bell with its coded tap-out. Voice annunciation in three tiers. The reset point at the watch desk. Night mode. Auto-clear, failed-to-respond retone, and scheduled tones. |
|---|---|
| The board | The call board on any wall, per-board filters, priority colors, the unit strip, live run timers, held cleared calls, explicit CAD link states, and texture replacement onto an MLO's own television. |
| Your department | Stations, apparatus rosters and footprints
created in game, or kept in config.lua if you keep your server in
version control. |
| Feeds | The demo feed, which ships enabled, and Sal's Kewl CAD on the same server. |
| Open seams | Ten integration hooks, all of them fired, shipping readable and editable outside the escrow. The framework adapter too. |
A station with no devices can be toned and nothing will happen. That is the commonest thing to go wrong on a new install and it is not a fault: this resource does not know where your bay doors are until you tell it.
Make a station, walk its footprint, then stand where each light, speaker,
door and whistle belongs and add it. stationalert status in the
server console names every house of yours that has nothing in it. See
Devices.
Everything is built in game, in one panel.
/stationalert has five tabs: SCREENS, STATIONS, DEVICES, SOUNDS
and SEQUENCES. Nothing about your firehouse needs a command typed into chat,
and nothing needs a coordinate typed into a config file.
The commands are all still there, and they are not deprecated — a server
console has no panel, a deployment script has no hands, and an owner who has
edited html/ and broken the builder still has to be able to run
their firehouse.
Requirements
| Server | FiveM, any recent artifact |
|---|---|
| Framework | qb-core, qbx_core, es_extended, or none at all |
| Dependencies | None. |
The framework is detected at runtime. On a standalone server permissions fall back to the ace alone and everything else works normally — a station television on a standalone server is a perfectly reasonable thing to want.
Optional integrations are also detected at runtime and are deliberately not listed as dependencies. Listing an optional resource as a dependency makes the whole thing refuse to start on a server that does not run it, which is the opposite of optional.
Installation
-
Drop the
sals_kewlstationalertfolder into yourresources/. -
Add it to your
server.cfg:ensure sals_kewlstationalert -
Give yourself permission to build. Placing boards, creating stations and adding devices change the map, so all three are ace-gated — and nothing grants the ace by default.
The quickest way, with the server already running and you in game. In the server console:
stationalert ace # who is online, and who has it stationalert ace 1 # grant it to player 1That takes effect immediately. No restart of the server, no restart of the resource, and no reconnecting. If your builder is already open, press RE-CHECK on the banner in it.
The grant is live but not saved. The command prints the permanent line; put it in
server.cfgor it is gone on the next restart:add_ace identifier.license:YOURS sals_kewlstationalert.admin allowIn game,
/stationalert acetells you your own id and the exact line.Why the identifier and not a group?
add_ace group.admin sals_kewlstationalert.admin allowworks perfectly well — but only if you are actually ingroup.admin, and being a txAdmin admin does not put you there. That is the single commonest thing to get wrong here, so the command grants straight to your identifier: one line, and it cannot be wrong.Skip all of this and the builder still opens, as VIEW ONLY: you can change what an existing board shows, and every build button is dead. It says so, names the ace, and gives you the command.
-
Restart, and check it came up. In the server console:
stationalert statusYou should see the demo provider running, a healthy link, a handful of calls, and your stations.
Keep the data/ folder when you update.
data/stations.json holds your fire department — every house,
its apparatus, its footprint and every light, door, speaker and whistle in it
— and data/screens.json holds your board placements. They are
yours, not ours.
A download ships an empty data/, so copying a new version
over the old one folder-and-all is how somebody loses their firehouse. Copy
the files, or keep the folder aside and put it back.
Your first five minutes
A house that alerts properly, start to finish, without opening a text editor or typing a single command beyond the one that opens the builder.
- Stand in your apparatus bay and run
/stationalert. The builder opens. - STATIONS — + STATION. Give it an id
(
sta1) and a name (Station 1). The id is what everything else stores and never changes; the name is what the board says and can change whenever. - Tick off your apparatus. The checklist is built from the callsigns the feed already knows about, so you are picking from a list rather than typing. Anything not on it yet can be typed in.
- Walk the footprint. Press WALK IT. A ring is drawn on the ground and follows you; press SPACE to drop the middle where you stand, Q and E to size it until it covers the building, then ENTER. See Stations for why this one matters more than it looks.
- DEVICES — + DEVICE — QUICK FIT. This is the step that
makes the house alert. You are walked through five things one after another
— a bay rotator, bay speakers, a bay door, the roof whistle and the reset
point at the watch desk. Each one shows you what it will do while you
position it; walk it where it belongs and press
ENTER.
About ninety seconds. See Devices. - TEST IT. Click any device in the list and press TEST IT. The light runs, the speakers drop the tones, the whistle winds, the bay door opens and closes itself. This is where you find out that the speaker is inside a wall, rather than an hour later.
- SEQUENCES — TONE IT. Pick the house and set it off for real. Lights, ramp, tones, doors, whistle, and the announcement on the board. Press the reset point in the world, or SILENCE, to stop it.
- Place a board. Back in SCREENS, press + PLACE. You are handed a live board already showing calls. Walk it onto the dayroom wall, square it up, press ENTER.
- Leave it running. The demo feed opens a new call every
half minute or so, and with
Config.Alerting.Autoon — it ships on — the house tones itself when one of your own trucks is assigned. Press DISPATCH A DEMO CALL if you would rather not wait.
Steps 5 to 7 are the resource. Everything before them is telling it what your fire department is, and everything after is the television on the wall. If you only do one thing after installing this, do QUICK FIT and then TONE IT.
If the board does not appear at all, run
/stationalert winding and cycle it — see
Troubleshooting.
Stations
A station is a house: a name, the departments that run out of it, the apparatus that belongs to it, and where it is.
Why a station has a footprint
A station record did not originally have coordinates, and the argument for giving it some is not that a record looks tidier with a position on it. It is that four things cannot be answered without one:
- The turnout clock — apparatus crossing the footprint is what stops it. That is the number a fire department actually cares about.
- "In quarters" — whether somebody is inside the house, which decides whether they hear the walls or their pager.
- Acknowledge proximity — who is close enough to silence the whistle.
- The blip on the map.
A station with no footprint still matches calls by callsign exactly as it would otherwise, and simply cannot answer those four questions. Nothing breaks; the builder tells you what is missing.
The apparatus roster
The roster is how a call becomes this house's call: any call with one of these callsigns assigned to it is yours.
Callsigns are matched loosely — case and punctuation are ignored — because a
CAD and a human rarely agree on whether it is E1,
e-1 or Engine 1. All three match the same truck.
The builder shows a checklist of the units that exist,
read from the feed, rather than a text box. Typing E1 into a game
menu is genuinely worse than editing a config file; picking three trucks off a
list is genuinely better.
Keeping your houses in version control
Stations can also be listed in Config.Stations, which is the
right choice if your server lives in git. Those are locked in the
builder: visible and tunable, but not renamable or deletable in game,
so nobody can quietly remove a house that the next restart puts back.
If an id appears both in config.lua and in
data/stations.json, the config one wins and the
console says which ids collided. A file somebody deployed outranks a file
somebody clicked.
Devices: what a house alerts with
A station on its own is a name and a roster. A device is a thing in that station that does something when the house is toned: a rotating light, a run of LED strip, a bay door, the whistle on the roof, the speakers in the bunk room, the reset button at the watch desk.
A house with no devices can be toned and nothing will
happen. The activation is accepted, checked and journaled, and then
there is nothing to run. stationalert status names every station
of yours in that state, out loud, because this is the commonest thing to go
wrong.
Making one
Open /stationalert, go to DEVICES, pick the
house, and press + DEVICE.
What opens is a palette rather than a form: cards that say what a thing is for. Bay rotator — the red beacon in the apparatus bay. Sweeps, and takes its color from the call. Pick one and you are put in the world holding it; walk it where it belongs and press ENTER.
That is the whole job. A device placed off the palette needs no further editing to be correct — every field it has is already set to something sensible for the thing it is. You can change all of them afterwards, because a template is a starting point and not a type.
QUICK FIT is the button to press on a new house. It walks you through the five things a house cannot alert without — a rotator, bay speakers, a bay door, the whistle and the reset point — one after another, without stopping to reopen the panel between each one.
About ninety seconds of walking round your own station, and the house alerts properly at the end of it.
The preview is the real device
A rotator in the wrong place is invisible until an alert happens, and then it is wrong in front of everybody. So while you are positioning something, it does what it will do:
| a light | runs, at its real color, rpm and reach |
| a strip | draws the run you have walked, with its emitters at the real density |
| speakers, a bell | draw their radius as a ring that follows the ground |
| the whistle | the same, and it is 300 m across, which is the correct thing to be alarmed by |
| a bay door | draws the clearance volume it will never close on |
| a reset point | draws how close you have to stand to press it |
The keys are the ones the board placer uses, because somebody who has placed a board should not have to learn a second tool: WASD to move, Q and E for up and down, the arrows to turn, SPACE to bring it back to arm's length in front of you, SHIFT to go faster, ENTER to place, BACKSPACE to cancel.
TEST IT
The single most useful button in the builder. It makes that one device do its thing, right now, on its own — so nobody places a speaker and finds out an hour later that it was inside a wall.
It is not a real alert: nothing is journaled as a run, nothing dedupes against anything, and it cannot be confused with the house going out. A door is the exception and has to be, because a door is operated by the server: testing one really opens it, and it closes itself on its own timer exactly as it would in a sequence.
Seeing what a house already has
While the DEVICES tab is open, every device in the selected house is drawn in the world, colored by kind, with the selected one showing its reach. A list of nine ids means nothing; nine markers in a bay means everything.
They follow the panel's selection rather than your position, so you can walk out onto the ramp and look back at where the whistle is. The MARKERS button turns them off for somebody taking a screenshot.
A house that cannot alert says so. In amber, at the top of its own list, naming what is missing — and specifically, because "no speakers" is actionable and "misconfigured" is not. With no speakers there are no tones, no ramp and no voice at all.
Where they are stored
In data/stations.json, beside the station they belong to,
because a house and the things in it are one thing you back up or copy to your
test server. Two files that have to be kept in step is two files somebody will
get out of step.
Doing it from a console instead
Every one of these is also a command, and they are not deprecated — see
Commands. A server console has no panel, a deployment
script has no hands, and a broken html/ must not take the
firehouse with it.
The kinds
| kind | what it is |
|---|---|
light | A rotator. Any color, any rpm, and either a sweeping beam, a hard flash or a steady lamp. Optionally washes the whole room in its color. |
strip | LED strip. A run, not a point — see below. |
door | A bay door or an apparatus gate. Five drivers — see below. |
siren | The whistle. Outdoor, heard across the town, default 300 m. |
speaker | House speakers. Indoor, default 40 m, so they fill a station and do not leak into the street. The tones, the ramp and the voice all come out of these. |
bell | A struck house bell. Quieter than the whistle, and what the coded tap-out uses. |
reset | The acknowledge point at the watch desk. Press E and the noise stops. |
indicator | A small steady or flashing lamp. For a status light over a doorway. |
relay | Fires an event or an export of your own with the alert attached. How you light your own sign or drive something we have never heard of. It is the only kind with no position. |
Zones
Every device has a zone — bay,
bunk, dayroom, anything you like — and a sequence step
can name a zone instead of the whole house. It is a field on the device form.
This is how you light the apparatus bay red for a medical without waking the
bunk room, which is what most departments actually do. The shipped
ems sequence does exactly that.
A strip is a run
That single fact drives its whole design. It has a path, a direction along that path, and effects that travel it.
So placing one is walking it. Pick a strip off the palette and the tool puts you at one end; press SPACE at each corner, BACKSPACE to take the last corner back, and ENTER when you reach the other end. The run and its emitters are drawn as you go, so what you walk is what you get.
The emitters redistribute themselves evenly along the whole path by length, so a run with one long leg and one short one has evenly spaced lights rather than a cluster at the corner. MOVE re-walks a run you want to change.
Six effects: solid, flash, pulse,
chase (a lit segment traveling the run), wipe, and
alternate — two colors in blocks, which is the red-and-white bay
strip every apparatus floor in America has.
The emitter budget is capped and the cap is enforced when you author, not when you run. A bay wrapped in a four-hundred-emitter strip is a support ticket that says "this resource tanks my FPS", and that is a ticket worth designing out.
Bay doors
The only device here that can ruin somebody's night, so it has the strictest rules in the resource.
Place the door in the doorway, then pick how it opens from the dropdown on its form. Drivers are detected at runtime and none of them is a dependency, so the resource never refuses to start on a server that does not run your doorlock.
| driver | how it opens |
|---|---|
native | The game's own door system, for MLO doors registered with it. Needs the doorsystem hash. |
ox_doorlock | Its own export. Needs the lock id. |
qb-doorlock | Its own event. Needs the lock id. |
pose | A plain object moved between a recorded closed position and a recorded open position. This is the one for sliding gates and roll-ups in MLOs with no door entry at all, which is most of them. |
event | Fires an event you name. The escape hatch for the one custom gate script you wrote. |
The form then asks for whatever that driver needs — a doorsystem hash, a lock id, or nothing — and shows no rows for the drivers you did not pick.
The pose driver has nothing sensible to type, so you record it instead. Leave the door shut and press RECORD CLOSED; open it and press RECORD OPEN. The form says which of the two is still missing until both are set.
Set the object's model as well when a bay has more than one identical roll-up, or they all find the same object.
The rules doors follow
- A door never closes on an entity. The volume underneath is checked first, from the server's own entity list and from the clients near the door. If something is there the close waits and retries, and past the give-up time the door is left open and it is logged. Crushing somebody's ladder truck is worse than an open door, every time.
- Doors close themselves. On
autoCloseSeconds, default 90; when the alert clears; and when the resource stops, so a restart never leaves a county of bay doors standing open. - Server-authoritative, one writer. There is no event a client can send that opens a door. Two drivers have to be applied on the client because the natives they use only exist there, but applying a state the server already decided on is not the same as deciding it.
- Rate limited per station, independently of the alert path, so a misbehaving CAD cannot cycle a bay door at 4Hz even when every other check has passed.
TEST IT on a door's form opens it for real, which is the
fastest way to check you wired one correctly. It closes itself on the timer on
the same form. From a console,
/stationalert doors <station> open|close does the same.
Sequences: what happens, and when
A sequence is a list of steps, and each step has an offset in seconds from the moment the house is toned. That is the whole idea.
['fire-structure'] = {
match = { agency = 'fire', priority = { 1, 2 } },
steps = {
{ at = 0.0, device = 'light', action = 'on', color = 'priority' },
{ at = 0.0, device = 'strip', action = 'flash' },
{ at = 0.0, device = 'screen', action = 'alert' },
{ at = 0.0, device = 'speaker', action = 'rampup', profile = 'gentle' },
{ at = 3.0, device = 'door', action = 'open' },
{ at = 6.0, device = 'siren', action = 'blast', seconds = 12 },
{ at = 6.0, device = 'speaker', action = 'toneout', tones = { 'STATION1' } },
{ at = 14.0, device = 'speaker', action = 'announce' },
{ at = 24.0, device = 'speaker', action = 'announce' },
},
},
They live in Config.Sequences. Seven ship, and they are meant to
be edited rather than admired:
default | Required. Runs when nothing else matched. Deliberately modest: lights, a ramp, tones, doors. No whistle — a house that blows the town whistle for a lift assist gets complaints. |
|---|---|
fire-structure | A working fire, priority 1 or 2. Everything, including the whistle and the voice twice. |
fire-service | Fire, priority 3 and below. No whistle. |
ems | The bell instead of the whistle, the soft tones, and the lights only in the bay. |
police | The board and a light. Nobody annunciates a traffic stop. |
test | What /stationalert test runs.
Fifteen seconds, and it exercises every kind of device so you can hear that
they are all wired. |
whistle-only | One blast. What a scheduled noon whistle uses. |
Five things to know before you edit one
- A step names a device KIND, not a device.
device = 'light'means every light in the house being toned, so one sequence works in a station with four rotators and in a station with one. Narrow it withzone = 'bay', or withonly = { 'sta1.door.bay1' }when you really do mean the one door. - Matching is first match wins, and
defaultmust exist. If you delete it the resource puts a minimal one back and says so loudly in the console, because an unmatched call has to alert something. Silence is the one failure a fire dispatch cannot have. - Offsets are seconds, they are decimals, and they may
repeat. Two steps at the same
athappen together and that is normal. - A step that fails is just a step that failed. No step blocks another, none retries, and none can abort the sequence. Your doorlock resource being down means the doors do not open. It does not mean the tones do not drop.
- The whole timeline is sent at once and every client runs its own clock against it. That is what makes a player who walks into the bay eight seconds in join the sequence already in progress: they see the lights that came on at zero, they do not hear the tone-out that finished at six, they hear the whistle that is still blowing at the point it has reached, and they get the voice that is still to come. A client that hitches for 400 ms cannot hear the whistle after the voice.
Which one runs
Rules are checked most specific first, not in the order you wrote them — a Lua table has no order, so ordering it by how specific each rule is makes the answer the same on every restart. A rule naming a call type beats one naming a priority, which beats one naming an agency, which beats one naming only a station. Ties break on the name.
A rule can match on agency, priority,
codes (your CAD's own call-type codes, which is the most precise
rule there is) and stations. Set priority = <number>
on the sequence itself to force the order by hand.
The SEQUENCES tab shows all of this. One card per sequence with its steps drawn on a timeline, its match rule in the words the config uses, and how long the whole thing runs.
The timeline is the reason the tab exists. Reading a Lua table of offsets does not tell you that the whistle comes eight seconds after the lights; a bar chart tells you in a second, and it draws the two steps that genuinely last — the ramp and the whistle — at their real length.
It is read-only, and deliberately. A sequence is config a
server owner version-controls and reloads, and a timeline editor that writes
Lua back to disk is a much larger promise than this makes. Edit
Config.Sequences and restart the resource.
The same tab has TONE IT: pick a house, optionally force a
sequence by name, and set it off. It goes through the same one funnel a CAD
does, so it is rate limited, journaled and hooked identically — the builder
gets no exception. From a console, /stationalert sequences prints
the same list and the match order.
Every action
| kind | actions |
|---|---|
light, indicator | on,
off |
strip | on, flash,
off |
door | open, close |
siren | blast — with
seconds, blasts, gapSeconds,
profile |
speaker | rampup (a
profile), toneout (a list of tones),
announce, say (your own text),
off |
bell | ring (strikes),
tap (a code), off |
screen | alert,
normal |
relay | fire |
all | off — the panic button, in
sequence form |
Sequences are validated when the resource starts, step by step. A step that
names a device kind that does not exist, or tells a door to
blast, is dropped with a console line naming the sequence and the
step number — and the rest of that sequence still runs. A typo
in your door step must not cost the house its tones.
Tones, the ramp, the whistle and the bell
No audio files ship with this resource and none are
streamed. Every sound below is a recipe in
Config.Audio — a description of a sound — synthesized in a
browser page at the moment it is needed.
Three things that buys you: nothing to download, no "this resource is too
large" warning, and no audio licensing question, ever. It is also why you can
change what a tone sounds like by editing a number in
config.lua.
The tones
Two-tone paging, in the shape every American firehouse knows: an A tone for a second, then a B tone held for three. The numbers that ship are real Motorola Quick Call II frequencies.
STATION1, STATION2 | Two-tone paging. Add your own by copying one and changing the frequencies |
ALLCALL | One long tone, which is what a county drops when it wants every house listening |
ALERT | Three short attention beeps, before a voice announcement |
SOFT | A quiet two-tone, for EMS at three in the morning |
The ramp
A rising signal over three to eight seconds that wakes a sleeping house without the cardiac event. It is its own thing and not a volume fade over the tones: the pitch does not move, the level does, and a pulse rate makes it insistent without making it loud. It stops the moment the tones drop.
Three profiles: gentle (six seconds, pulsing),
fast (two and a half), and hum (eight seconds, no
pulse at all — the one people ask for after a week of the pulsing one).
The whistle
The old-time fire whistle, and a different animal from the house speakers: outdoor, heard across the town, with a long mechanical wind-up and a longer coast down.
The coast down is the part every cheap imitation gets wrong, so it is the part modeled most carefully. The pitch rises over seven seconds, holds, and then sheds it over eleven — longer coming down than it took going up, because a motor losing speed drops pitch fast at first and then lingers. That lingering bottom end is the whole sound of a fire whistle.
steam | Long coast down, and the pitch wobbles, because a steam whistle never holds a perfectly steady note. The default |
electric | Clean rise, sharper cut |
airhorn | A blast, not a wind-up |
euro | Two-tone hi-lo |
Blast patterns are part of the step: seconds,
blasts, gapSeconds. Three blasts means the box, which
is how a real whistle signaled.
The bell, and the coded tap-out
A struck bell — several inharmonic partials that decay at different rates, which is why it sounds like metal rather than like a sine wave.
action = 'tap' with code = { 2, 3 } strikes twice,
pauses, and strikes three times: box 23. That is how a house was
told where the fire was before anybody had a radio, and it costs nothing to
keep.
Hearing them before you choose
The SOUNDS tab lists every tone set, ramp, whistle and bell this server has — ours and any you have added — each with a name, a sentence saying when you would want it, and a PLAY button.
Choosing a tone set out of a dropdown you have never heard is guessing. Pressing a button next to each one and picking the one you like is not. It plays for the person who pressed it and nobody else.
What one house sounds like
A sequence step has to name some tone set, because it has to say something. A house that has chosen its own overrides it.
That is on the same tab: pick a station, and set its tone set, its whistle profile, and whether it annunciates at all. Any of the three can be left as "whatever the sequence asks for", which is the default and what every station did before this existed.
This is the setting that makes one sequence work in four stations. Without it, a department with four houses needs four nearly identical sequences whose only difference is which pair of frequencies drops and which whistle blows — and that is how a config file becomes something nobody maintains.
How loud, and to whom
Every device has a radius. Inside it, how loud a sound is for one player is worked out from the distance, whether they are indoors, their own volume setting, and night mode — and it is re-worked while the sound plays, so somebody driving past the station during a whistle hears it swell and fade.
Indoors a whistle on the roof is muffled, not absent. That distinction is most of the feel of the thing.
The player's own volume
A slider at the top of the SOUNDS tab, or /stationvolume 40.
No permission and no server round trip, because it is their ears and nobody else's business. It is remembered on their machine and applied to whatever is already playing, so turning the whistle down while the whistle is going turns the whistle down.
Night mode, which ships ON
Between 22:00 and 06:00 game time, the level is capped and every ramp becomes
the gentle one. Config.Alerting.NightMode.
A full-volume tone-out at 0300 is a genuine complaint on a server with a night shift. Some servers will switch this off, and they should have to choose to.
If you already have an audio system you would rather use, open
integrations/hooks/client/audio.lua. Return true and
you own that sound; the built-in synthesis stays quiet for it. It is per
sound, so you can put the tones through your own player and leave the whistle
and the voice to us.
Voice annunciation
"Station 1, structure fire, 2400 block Alta Street, apartment building, Engine 1, Ladder 1, Battalion 1. Time out 0347."
Read twice, the second time after the tones have stopped ringing. The address is read the way a dispatcher reads one — "2400 block Alta Street", not "two thousand four hundred Alta".
Three tiers, and it falls down them on its own
| tier | what it is | what it costs |
|---|---|---|
tts | Hosted text to speech. Real speech, any street name, any dispatcher note | An API key, and fractions of a cent per new phrase |
bank | The phrase bank: short clips in
audio/bank/, assembled from slots | Recording or buying about 120 clips, once |
text | No audio. The announcement is written on the board and shown on screen to anybody in quarters | Nothing |
Config.Voice.Tier ships as 'auto', which uses the
best one available. With nothing configured you get the text tier.
The words always arrive. With no clips and no API key the announcement still reaches every board in the station and every player inside the footprint — it is read rather than heard. That is a supported configuration, not a broken one, and it is also what makes the annunciation work for a player who plays with the game muted.
The phrase bank
No clips ship, and that is deliberate rather than lazy: a voice is a
licensing question, and the one we shipped would be the one every server sounded
like. audio/bank/ is yours.
The announcement is built as a sentence and as a list of slots, and the bank plays the slots back to back:
STATION_1 TYPE_STRUCTURE_FIRE DIGIT_2 DIGIT_4 DIGIT_0 DIGIT_0 BLOCK_OF
STREET_ALTA_STREET UNIT_E1 UNIT_L1 TIME_OUT DIGIT_0 DIGIT_3 DIGIT_4 DIGIT_7
It sounds like a concatenative annunciator from 1994, because that is exactly what it is. That is the aesthetic, not a compromise. A firehouse annunciator has never sounded smooth and should not start now.
Record the ten digits first — every address and every time out comes out of
those, so ten clips gets you something usable. The full vocabulary, the file
format and the recording notes are in audio/README.md in the
download. A missing clip is skipped rather than an error, and a street the bank
has never heard of falls back to "at the reported location", which is what a
real dispatcher says when they cannot read the cross street.
Hosted text to speech
There are thousands of streets in this map and you will not record them all. That gap is the honest reason to pay for a voice.
Config.Voice.Tier2, off until you fill it in. Any endpoint that
takes JSON and returns audio works. The key is read from a convar, never
from a config file, so it cannot end up pasted into a Discord support
thread:
set sals_kewlstationalert_tts_key "your-key"
Two properties of it are worth knowing:
- Rendered once, cached forever, on disk, keyed by the exact
sentence. A server running 200 calls a night says "Alta Street" hundreds of
times and pays for it once. The cache lives in
data/voice/; deleting it costs you money rather than data. - Never on the hot path. Phrases are rendered when the feed opens the call, which is usually a minute before anybody tones it. A phrase still rendering when the announce step fires is skipped and a lesser tier reads. The tones never wait for the internet.
What gets read, and for whom
Config.Voice decides how many times it is read and how far
apart, and whether the station, the call type, the location, the units and the
time out are each included.
Config.Voice.Agencies is per agency, and
police ships false. Nobody annunciates a traffic
stop.
Silencing a house
A whistle nobody can shut off is not a feature. It is a support ticket at 4am.
The reset point
Put one at the watch desk:
/stationalert device add sta1 reset
Stand at it during an alert and press E. The whistle stops, the ramp stops, any announcement still to come is cancelled — and the lights and the board stay on, because the run is still running and a house that goes dark the moment somebody is awake is a house nobody can find their boots in.
Who it is stamped as, and how long they took, goes in the journal and to your
acknowledge hook. That second number is the one a chief actually
cares about.
Acknowledging is deliberately the most open action in the resource. No job and no ace: anybody physically at the station can shut the noise off. Somebody standing in the kitchen at 0300 should not need a permission, and being there is the control — which the server checks again for itself rather than believing the client.
What the reset stops is configurable, under
Config.Alerting.Acknowledge. It can also close the doors and kill
the lights if that is what your department does.
Before you have placed one
/ack works for anybody standing inside a station's footprint.
Leave it on until the reset points are in.
An alert always ends
Three ways, and one of them cannot fail:
- The call closes.
Config.Alerting.ClearWithCall, on by default, and almost always the right moment. - An admin ends it.
/stationalert silence sta1from the console. Different from acknowledging, and journaled differently. - The clock runs out.
Config.Alerting.AutoClearSeconds, default 180. The backstop that means there is always an end.
When an alert ends, the lights go out, the boards go back to normal, and any bay door that alert opened is closed.
Toning it again
One alert per house at a time. A second one is a retone: the first ends and the new one begins, rather than two timelines playing over each other in the same building. The board says RETONE.
A retone does not slam the bay doors shut and run them back up — the new alert adopts the doors that are already open.
Failed to respond
Nobody acknowledged and nothing left the house, so tone it again.
Config.Alerting.Retone, off by default: a
department that does not want it finds it infuriating, and one that does will
turn it on within a day.
The noon whistle
Real towns blow the whistle at noon whether or not anything is on fire. It is
a test of the whistle and the loudest piece of world-building in this resource.
Config.Schedule, off by default, in game hours.
Placing a board
You place a board by walking it onto the wall. There is no coordinate to type and no measuring to do.
| W A S D | Move it around |
|---|---|
| Q E | Raise and lower |
| Arrow keys | Rotate. Tap for coarse, hold for fine |
| SPACE | Aim it at me — the fastest way out of a mirrored board |
| [ ] | Narrower and wider |
| ; ' | Shorter and taller |
| SHIFT | Hold for four times faster |
| ENTER | Place it |
| BACKSPACE | Cancel |
What you are aligning is the real board. The preview is not a placeholder — it is a live screen running through the real renderer, showing real calls at the real size. What you line up is exactly what you get.
The one thing that confuses everybody
A board's heading is the direction somebody stands to read it, not the direction the board's own face points. Get it backward and the board renders mirrored, readable only from behind the wall.
You will never need to think about this, because SPACE aims the board at you and that is almost always what you want.
Sizing with a keyboard instead
Some server builds cannot read the bracket and quote keys. If yours cannot, the placer says so, and you set the width and height numerically in the builder panel afterward — which is easier anyway for getting a board to exactly 1.60 m.
Boards from config.lua
Boards can be listed in Config.Screens as well. Like config
stations they are locked: an admin cannot move or delete them in game, so the
furniture stays where your deployment put it. What such a board
shows can still be changed in game, because that is operations rather
than construction.
What a board shows
The filter is the product. A firehouse does not want the whole county's traffic stops on its dayroom wall.
Every board decides for itself, so the one in the dayroom and the one in the apparatus bay do not have to agree.
The important setting
agency |
Everything your station's departments are running, with your own runs pulled to the top and highlighted. The default, because a board that is blank most of the evening is a boring wall. |
|---|---|
assigned |
Only calls this house is on. Truer to a real alerting board. The screen shows IN QUARTERS the rest of the time. |
all |
Everything in the CAD, police included. |
The rest
- Agencies. Only these call kinds. Empty means whatever the station's own departments are.
- Priorities. Only these. Empty means all of them.
- Rows. How many calls fit before the board stops listing
and says
+3 more. Depends how big your board is, so it is per board. - Hold cleared. How long a closed call stays on the board, grayed and marked CLEARED, so somebody walking in from the bay sees that the last run just cleared. Zero hides them immediately.
- Unit strip. Your apparatus along the bottom, in the
order the station lists them —
E1,L1,BC1, and not alphabetically, because that is the order a firehouse thinks in. A truck the CAD does not know about still appears, as unavailable: a missing truck is information.
When the CAD link drops
The board keeps showing the last calls it had and marks itself: the header
goes amber and says CAD LINK SLOW, then NO CAD LINK.
It never blanks itself. A display that goes empty when its link drops is a display claiming there are no calls, and that is the one wrong answer a status board must never give.
Painting onto the MLO's television
If your station interior has a television whose texture can be replaced, the board can be painted onto that — real bezel, real glass, nothing to line up. It is better when it works.
It is an override rather than the default because it needs the model's texture names, which are specific to that MLO and have to be found or shipped as a preset. Pick one from the list in the builder, or name the model, texture dictionary and texture directly.
Three things to know before you use it
- It is per model, not per prop. The swap applies everywhere that model appears on the map. Two stations whose interiors use the same television model will show the same board — whichever claimed it first. The answer is to use a rectangle for one of them.
- It cannot report failure. A wrong texture name fails silently, so the checks are done beforehand and anything that does not pass falls back to drawing a rectangle.
- An MLO update can break it. A renamed texture would blank the screen. It falls back instead.
The fallback is the point. Keep the position and size on a replace-mode board, because that is what it draws if the texture is not there. A misconfigured override costs you a bezel, never the board.
Adding a preset
Presets live in data/mlo-presets.lua and adding one is a data
change, not code. You need three names: the model, its texture dictionary, and
the texture on the screen face. OpenIV or CodeWalker on the MLO will tell you.
If you find working names for a popular station MLO, send them over and they
will ship for everybody.
Where the calls come from
The demo feed, which ships enabled
Config.Feed.Provider is 'demo' out of the box. It
invents plausible fire and EMS traffic: real dispatch codes, a believable mix
that is mostly medical with the occasional working structure, and units that
are assigned, go en route, arrive on scene and then clear.
This is a supported feature, not a stub to be deleted. It is how you evaluate the resource before you own a CAD, how you check a board is lined up before wiring anything to it, and how you shoot a video of your station without waiting for a real structure fire.
It assigns your apparatus, read from your stations, so the "this house's run" highlighting works without you configuring anything.
NewCallEvery | A new call opens somewhere in this window, in seconds |
|---|---|
CallLife | How long a call lives before it clears |
MaxCalls | Never more than this at once, so it looks like a firehouse and not a disaster movie |
StartWith | How many are already running at boot, so a board is never blank the first time somebody walks past |
Sal's Kewl CAD
Set Config.Feed.Provider to 'cad'. That is all —
it reads the CAD on the same server, with no keys and no URLs.
It finds either half of the family:
sals_kewlcadbridge (CAD 2.x, where the in-game
resource talks to a hosted service) or sals_kewlcad (1.x). The
console says which one it found, because that is the first question worth
asking when a board is empty on a server that has both installed.
Calls arrive when they happen, pushed by the CAD rather
than waited for. The poll stays on as a backstop, because one missed push
would otherwise leave the board wrong until the next call came in. You can
raise PollMs once you are event-driven, and
stationalert status says which of the two is actually
happening:
cad link up, board pushed by event, 3s old
If the CAD is not running at all, the resource says so once and falls back to the demo feed rather than showing an empty wall.
If the CAD is running but too old, the board goes to NO CAD LINK rather than back to the demo feed. That is deliberate, and it is the opposite of the other fallback: on a live server a wall confidently showing invented calls is worse than a wall admitting it has nothing. The console names the export that is missing.
Departments the CAD knows and this resource does not
A CAD deals in more kinds of department than a firehouse does —
sheriff, state_police, whatever your server
invented. A call whose kind matches none of your stations' departments shows
on boards set to "every call in the CAD" and tones
nothing.
That is on purpose. Mapping an unknown department onto a default would mean
a firehouse woken at three in the morning for a highway stop, which is worse
than a call no board claims as its own.
Config.Feed.Cad.AgencyMap is where you say that sheriff means
police to you; it ships with the obvious three already in it.
Letting the CAD tone a house
Dispatch presses the button in the CAD and the firehouse goes out. On by
default; set Config.Feed.Cad.AcceptToneOuts = false if you would
rather only tone houses from in game.
A tone-out from the CAD goes through exactly the same checks as
every other way of setting a house off — both rate limits, the
dedupe, your authorize hook, the journal. It is not a side door,
and a CAD stuck in a retry loop hits the same limit a misbehaving script
would. See Setting a station off.
The acknowledgement goes back the other way: when somebody presses the
reset at the watch desk, the CAD is told who it was and which units are on the
call, so a dispatcher watching the board sees the house answer. That happens
in integrations/hooks/server/acknowledge.lua, which is the one
shipped hook that actually does something — so if you want to send something
else, or nothing, it is one file and it survives an update.
Which house did the CAD mean
The CAD identifies a station by its own id; this resource identifies one by the id you typed in the builder. Three ways of joining them up, tried in this order:
| The slug | The CAD gives every station a slug, and it is
deliberately the same shape as a station id here. Name the house
sta1 in both places and there is nothing to configure. This
is the way it is meant to work. |
|---|---|
| A map you write | Config.Feed.Cad.Stations, for when they do not match.
Keyed by the CAD's slug or its uuid — paste whichever one its owner panel
showed you:
|
| The name | Off by default.
Config.Feed.Cad.MatchByName = true matches the CAD's station
name against yours. |
Why matching by name is off. Two houses called "Station 1" will exist on somebody's server — one per agency is a completely normal way to name them. Toning the wrong firehouse is the worst thing this resource can do, so you have to ask for this, and even switched on, a name matching two houses is refused rather than guessed at.
The one mistake neither system can catch. Nothing stops
a CAD slug that is perfectly valid from colliding with a
different station's id here: the CAD has a house slugged
sta1, you have a sta1, and they are two different
buildings. Both sides think they agree and the wrong house goes out.
That is inherently your mapping rather than anything either resource can detect. When you set a slug in the CAD, check it against the station ids on your STATIONS tab.
A tone-out that matches nothing is refused and said out loud, naming the station and the slug it arrived with. A tone-out thrown away in silence is a firehouse that did not go out with nobody knowing why.
A tone-out that arrives before its own call reaches the board still works: the call is built out of the tone-out itself rather than refused.
Polling and staleness
PollMs | How often the provider is asked. The feed only sends clients what changed, so a short poll is cheap. 2000 is the default. |
|---|---|
StaleSeconds | No answer for this long and the board goes amber |
DeadSeconds | Past this it stops claiming the calls are current |
Setting a station off
Four things can set a house off: the feed automatically, a dispatcher in game, a web console, and somebody else's CAD or app. All four land on one server function. No surface has its own path, none can skip the checks, and the journal has one shape.
What happens, in order, every time:
- The request is checked for shape, and the station has to exist.
- Your
authorizehook decides whether this caller may tone this house. - Two independent rate limits. Per station, which protects the players in the building no matter how many things are asking. Per source, which catches one caller stuck in a retry loop. Either limit on its own leaves the other hole open, which is why there are two.
- Deduplication. One alert per station per call, for the life of that call — not a time window. Feeds re-poll, providers glitch, and a CAD reassigning a unit can hand the same call back as though it were new. A deliberate retone is a separate act and is exempt.
- Your
activatehook is told it is happening, before anything would make a noise. - It is written to the journal, accepted or refused.
Only then does the sequence engine pick a sequence, build the timeline and push it to everybody who can see or hear the house. Everything above happens first, every time, whichever of the four surfaces asked.
By hand, that is:
/toneout sta1 -- the one to bind
/stationalert tone sta1 -- the same thing, spelled out
/stationalert test sta1 -- a fifteen-second drill
With no call named, the house's current run is used — which is what a dispatcher means nine times out of ten. With no run either, it is an all-call with no call attached, which is a legitimate thing to want.
/stationalert journal prints every activation, accepted or
refused, with who asked and why it went the way it did. It is the first thing to
look at when somebody says the house did not go off.
The line that does not bend
A caller names a station and a call. It never names a device, a door, a doorsystem hash or a lock id. There is no field anywhere in the API or the hooks that accepts one.
You ask for a house; the server looks up what that house has and decides what it does. That is not us being precious about an API — it is the difference between a station alerting resource and a remote door opener for anybody who can post JSON at your server.
Commands
| command | who | what |
|---|---|---|
/stationalert | job or ace | Open the builder |
/stationalert place [id] [station] | ace | Go straight to placing a board |
/stationalert move <id> | ace | Pick a board up and move it |
/stationalert delete <id> | ace | Delete a board |
/stationalert list | anyone | Every board |
/stationalert stations | anyone | Every station, and whether it has a footprint |
/stationalert status | anyone | Provider, link health, counts |
/stationalert demo force | job or ace | Open a demo call now |
/stationalert demo clear | job or ace | Clear the demo calls |
/stationalert winding | ace | Cycle the triangle winding |
stationalert status works in the server console and is the
fastest way to answer "it does not work" — faster than a screenshot. It reports
the feed, the link, the counts, every alert running right now, and
every station of yours that has no devices.
Toning a house
| command | who | what |
|---|---|---|
/toneout <station> [call] [sequence] | job or ace | Tone a house. The one to bind |
/stationalert tone <station> [call] [sequence] | job or ace | The same thing, spelled out |
/stationalert test <station> | job or ace | A fifteen-second drill that exercises every kind of device |
/stationalert silence <station> | job or ace | End the alert from the console |
/stationalert doors <station> open|close | ace | Move the bay doors by hand |
/stationalert sequences | anyone | Every sequence, its length, and the match order |
/stationalert journal [n] | job or ace | Every activation, accepted or refused, and why |
/ack | anyone in quarters | Acknowledge, where no reset point is placed yet |
/stationvolume [0-100] | anyone | This player's own volume. No permission, no round trip |
/stationalert ace | anyone | Your own id, whether you can build, and the line that would let you |
stationalert ace [id] | console only | Who has the ace, or grant it to a player live |
Building a house
The builder does all of this and is the way it is meant to be
done: /stationalert, the DEVICES tab, + DEVICE. See
Devices.
These are the same operations from a console, and they are not deprecated.
A server console has no panel, a deployment script has no hands, and an owner
who has edited html/ and broken the builder still has to be able
to run their firehouse.
| command | who | what |
|---|---|---|
/stationalert device add <station> <kind> [id] | ace | A device where you are standing |
/stationalert device list [station] | anyone | Every device, or one house's |
/stationalert device delete <id> | ace | Delete one |
/stationalert device move <id> | ace | Move it to where you are standing |
/stationalert device power <id> on|off | ace | Switch one off without deleting it |
/stationalert device zone <id> <zone> | ace | What a sequence step narrows to |
/stationalert device point <id> | ace | Another corner on a strip's run |
/stationalert device door <id> driver <driver> | ace | How that door opens |
/stationalert device door <id> hash|lock|model <value> | ace | What that driver needs |
/stationalert device door <id> pose closed|open | ace | Record where the door is right now |
Kinds: light, strip, door,
siren, speaker, bell, reset,
indicator, relay. See
Devices.
The keybind
The builder is available as a keybind and it ships unbound. Assign it in the game's own keybind settings, under Sal's Kewl Station Alert. A resource that grabs a key on install is a resource fighting whatever you already had on that key.
Who may do what
Two levels, because moving a television and changing what it shows are different acts.
| Building | Placing, moving and deleting boards. Creating, editing and deleting
stations, including rosters and footprints. Adding, moving, configuring and
deleting devices.
The ace, sals_kewlstationalert.admin
— which nothing grants by default. See below. |
|---|---|
| Operating | Changing what an existing board shows — its filter, the unit strip,
switching it off. The demo controls.
The jobs in Config.Access.Jobs, on duty
if RequireOnDuty is set. |
A battalion chief rearranging the board is not an administrative act, so it does not need an admin. Taking Engine 1 off a station's roster is, because that decides which calls are that house's runs — which is to say it decides when the tones drop. So every station edit is ace, including the roster.
Devices do not split at all, and the reason is the bay door. A device list is the list of things the server will operate on somebody's behalf, so adding to it, moving something in it, or pointing one at a different doorsystem hash is construction every time. Switching one off could reasonably have been operations and is not, for the same reason: a bay door somebody switched off is a bay door that stops opening for calls, and whoever has to work out why should find it behind the same permission as everything else about that door.
Granting the ace
Nothing grants it for you. A fresh install has no principal the ace is allowed for, so the builder opens as VIEW ONLY for everybody until you grant it.
Live, with the server running:
stationalert ace # in the SERVER console: who is online, and who has it
stationalert ace 1 # grant it to player 1, immediately
An ace granted this way takes effect on the next permission check, which is the next thing that player does — so no restart, no reconnect, and no reopening the panel. Press RE-CHECK on the view-only banner and it goes away.
Permanently, because a live grant is lost on the next restart. The command above prints exactly this line for that player:
# server.cfg
add_ace identifier.license:YOURS sals_kewlstationalert.admin allow
In game, /stationalert ace tells a player their own id, whether
they have the ace, and the line. It grants nothing — a command a player could
use to grant themselves a building permission is a privilege escalation, and no
check on "are you already an admin" makes that safe, because the whole point is
that the person asking is not one yet. Granting is console-only, which means it
is whoever can already stop the server.
Why an identifier rather than a group.
add_ace group.admin sals_kewlstationalert.admin allow is correct
and does nothing on its own: it says what group.admin may do,
and you also have to be in group.admin. Being
a txAdmin admin does not put you there. That is the thing that
catches almost everybody, so the command grants straight to the identifier
instead.
The ace's name is Config.Access.AcePermission if you would
rather it were something else.
The builder grays out what you cannot do rather than offering buttons that always refuse, and the VIEW ONLY chip explains why and names the ace. That is a courtesy, not the check — every write is checked again on the server.
Configuration
Everything is in config.lua, commented in plain language. It
opens with the four things worth knowing before you change anything.
Config.Feed.Provider | 'demo' or 'cad' |
|---|---|
Config.Feed.Cad | Whether the CAD may tone your houses, and how its stations map onto yours. See Where the calls come from |
Config.Demo | How busy the invented traffic is |
Config.Filter | The defaults every board starts with |
Config.Board.PriorityColors | What a priority 1 looks like |
Config.Board.FlashSeconds | How long a new call flashes so somebody in the kitchen notices. Zero turns it off |
Config.Board.Clock24 | 24-hour, or AM/PM |
Config.Render.DrawDistance | How far away a board still draws |
Config.Render.DetailDistance | Past this it keeps drawing but stops updating every tick |
Config.Dui.MaxSurfaces | How many boards can be live at once. Four is plenty for a dayroom, a bay and a watch desk |
Config.Access | The ace and the jobs |
Config.Persistence.SaveDelay | Writes are batched this many ms after the last change |
And the alerting half:
Config.Sequences | What actually happens when a house is toned. See Sequences |
|---|---|
Config.Devices | Devices kept in version control rather than made in game |
Config.DeviceDefaults | What a device gets when it does not say otherwise, per kind |
Config.Audio | The tone, ramp, whistle and bell recipes, and the master gain |
Config.Voice | Which annunciation tier, what gets read, and how many times |
Config.Alerting.Auto | Whether the CAD tones houses on its own, on assignment or on call creation |
Config.Alerting.NightMode | Quiet hours, the level cap, and the forced gentle ramp. Ships on |
Config.Alerting.AutoClearSeconds | The backstop that means an alert always ends |
Config.Alerting.Acknowledge | What the reset stops, and whether /ack is allowed |
Config.Alerting.RateLimit | How often a house can be set off, per station and per caller |
Config.Alerting.DoorRateLimit | The same for doors, independently |
Config.Alerting.Retone | Failed-to-respond retone. Ships off |
Config.Alerting.BroadcastRadius | How far from a house an alert is sent at all |
Config.Schedule | The noon whistle. Ships off |
Config.Debug.Enabled ships false and should stay
that way. Turn it on only while you are working on something.
Your data
Two files in data/ are yours. Keep them when you update.
stations.json |
Your fire department: every station you created in game, its apparatus
roster, its footprint, and every device in it — the lights,
the bay doors, the speakers, the whistle, the reset point.
The devices share this file on purpose. A house and the things in it are one thing you back up, copy to your test server, or hand to somebody else, and two files that have to be kept in step is two files somebody will get out of step. |
|---|---|
screens.json |
Every board you placed — where it is, how big, and what it shows. |
voice/ |
Rendered speech, cached, one file per phrase. Deleting the contents
costs you money rather than data — every phrase is re-rendered the next time
a call needs it. Keep the folder itself.
It never ships: the packaging script refuses to build a release containing one of these. |
mlo-presets.lua |
Ours. Texture names for televisions in common station MLOs. Overwrite it on update. |
One more folder is yours, outside data/:
audio/bank/, where your voice clips go. Nothing
ships in it. See Voice annunciation and
audio/README.md.
The folder has to exist. The server cannot create it, so if
you delete it nothing you place survives a restart — and the console says so on
every save. mlo-presets.lua ships in there, which is what normally
keeps the folder around.
Neither JSON file is written until you actually create something in game, so
a fresh install having no stations.json is normal.
If one of them is corrupt
An unreadable file is reported in the console and left alone. The resource starts anyway with nothing loaded from it. That is deliberate: a station you cannot see is a bad afternoon, but a file quietly overwritten with an empty one is a lost afternoon.
Note the trap. Once the resource is running with nothing
loaded, the next thing you place saves over the broken file. If
stations.json looks wrong, copy it somewhere before you touch
the builder.
Records that can be read are kept. One unreadable station does not cost you the rest of them — it is skipped, named in the console, and everything else loads.
Editing the integrations
The integrations/ folder ships readable and
editable, outside the escrow, because it is meant to be edited. Read
integrations/README.md first.
A hook is a decision this resource refuses to make for you, because no two fire departments agree on the answer. Each one is a plain Lua file with working code in it that you can read.
| I want to… | Open |
|---|---|
| Use my own notification resource | hooks/client/notify.lua |
| Change which house a call belongs to | hooks/server/match.lua |
| Decide who may tone a station | hooks/server/authorize.lua |
| Post to Discord, feed my stats, page a talkgroup | hooks/server/activate.lua |
| Fit a framework we do not support | server/framework.lua |
Making your changes survive an update
Do not edit our files if you can avoid it — an update overwrites them, because the download carries the file names we wrote.
Instead, copy the file to a name of your own in the same folder. Both load, yours registers second, and yours wins. The console tells you which file won at boot. Our file gets replaced on every update and never touches your copy.
hooks/server/match.lua <- ours. Gets overwritten.
hooks/server/match-ourdept.lua <- yours. Survives forever.
The rules
- A hook gets data and helpers, never authority. It cannot
open a door by returning
true. - A broken hook is not a broken server. Every call is wrapped. A hook that errors prints once and is skipped; the station keeps working.
- A missing hook is not an error. Delete a file and that event does nothing.
- Any
.luayou drop intohooks/server/orhooks/client/is loaded. A file in the wrong one of those two folders is almost always what a mysterious error in here turns out to be.
Every hook
All ten fire. Nothing in the folder is a placeholder.
| file | fires when |
|---|---|
server/match.lua | Deciding whether a call belongs to a station |
server/authorize.lua | Deciding whether a caller may tone a house |
server/activate.lua | A request to set a house off, before anything happens |
server/alert.lua | The sequence has been scheduled, so it knows what will happen and when |
server/device.lua | A device was told to do something. Every device, every step |
server/acknowledge.lua | Somebody hit the reset at the watch desk |
server/cleared.lua | An alert ended, and why |
client/notify.lua | A message is being shown to the player |
client/alert.lua | An alert landed on this machine. Your screen effects go here |
client/audio.lua | A sound is about to play. A full override |
Two are worth reading twice.
device fires from the server, not the client,
and it fires for every selected device on every step — the lights coming on,
the tones dropping, the whistle winding up, the doors moving. The server is the
only place that can honor that, because it is the only place that sees the whole
house rather than one player's earshot. It fires a lot, in the moment when
timing matters most, so keep it fast and never block.
audio is the one hook that can take something
over. Return true and you played that sound yourself; the
built-in synthesis stays quiet for it. It is per sound, so you can put the tones
through your own player and leave the whistle and the voice to us. Respect
ctx.gain — it already has distance, occlusion, the player's own
volume and night mode in it.
Nothing else can take anything over. authorize can refuse an
activation and that is the only veto in the list.
Exports
Setting a house off
local alertId, err = exports['sals_kewlstationalert']:Activate({
station = 'sta1',
call = {
id = 'myalarm-42',
kind = 'fire',
type_label = 'ALARM ACTIVATION',
priority = 3,
location = '2400 ALTA ST',
assigned = { 'E1' },
},
})
call can also be the id of a call already in the feed, as a
string. Leave it out entirely for an all-call.
The invoking resource is stamped as the actor from
GetInvokingResource, so nothing can claim to be another resource
in the journal.
Reading the state
exports['sals_kewlstationalert']:Stations()
exports['sals_kewlstationalert']:StationAt(coords) -- which house is here
exports['sals_kewlstationalert']:StationCalls('sta1')
exports['sals_kewlstationalert']:StationCommitted('sta1') -- anything out?
exports['sals_kewlstationalert']:StationsForCall(call) -- who would get it
exports['sals_kewlstationalert']:ActiveCalls()
exports['sals_kewlstationalert']:Units()
exports['sals_kewlstationalert']:FeedHealth() -- 'ok'|'stale'|'dead'
exports['sals_kewlstationalert']:Screens()
exports['sals_kewlstationalert']:SetPower(id, on)
exports['sals_kewlstationalert']:Journal(20)
The alerting half
exports['sals_kewlstationalert']:ActiveAlerts()
exports['sals_kewlstationalert']:StationAlert('sta1') -- what is running here
exports['sals_kewlstationalert']:Acknowledge('sta1') -- stop the noise
exports['sals_kewlstationalert']:ClearStation('sta1') -- end the alert
exports['sals_kewlstationalert']:StationDoors('sta1', true)
exports['sals_kewlstationalert']:GameClock() -- hours, minutes, night
exports['sals_kewlstationalert']:Announce('sta1', 'text') -- pre-render a phrase
StationDoors takes a station and a direction.
It does not take a door, a doorsystem hash or a lock id, and there is no
export anywhere that does. The server looks up what that house has and
operates that.
What comes back when it says no
err_bad_station | You did not name a station |
station_unknown | No house by that id |
err_unknown_call | That call id is not running |
err_rate_limited | Too many, too fast |
err_already_toned | Already toned for that call |
no_permission | Your authorize.lua said no |
err_no_devices | Request was fine, but that house has no lights, doors, speakers or whistle to run |
Every attempt is journaled either way, so you do not need to log them yourself to find out what happened.
Languages
No user-facing string is hardcoded. locales/en.lua is the
baseline.
To add a language, copy it to locales/xx.lua, change the table
key at the top, translate the right-hand side, and set
Config.Locale. Missing keys fall back to English rather than
showing a blank, so a half-finished translation is usable.
locales/ ships outside the escrow, so your translation survives
as a file you own.
Performance
Every board is a browser page, and a browser page costs memory like a browser tab. So the resource is built around not having many:
- A fixed pool of surfaces,
Config.Dui.MaxSurfaces, created on demand and handed to the nearest boards in view. They are taken back the moment you walk away, so the pool follows the player rather than staying on whichever board was seen first. - Nothing renders for a board nobody is near or facing. Out of range, out of the camera, or behind the wall — all three cull.
- Call data is only sent to players standing at a board. A server where nobody is in a firehouse sends no call traffic at all.
- Boards repaint on change, not on a timer. Run timers and the wall clock tick inside the page itself, so a running clock is not a reason to rebuild anything.
Measured figures will be published here before the full release. The design targets above are what the resource is built to; putting numbers next to them needs a populated server rather than a test box.
Troubleshooting
The board is blank
Run stationalert status in the server console. If the provider
says demo and there are active calls, the feed is fine and the
problem is rendering — try the next item.
No board appears at all
Run /stationalert winding and cycle through the three settings,
keeping whichever shows the board.
Which way round a triangle has to be wound to be the front face is a
property of the server build and cannot be asked from a script, so it is a
setting. The default (both) draws every triangle twice and always
works; the other two draw it once and halve the cost. This is about cost, not
about getting a picture.
If the console mentions DrawSpritePoly, your server build is
too old to draw boards at all.
The board is mirrored
Its heading is backward. Move it and press SPACE to aim it at you.
Two boards are showing each other's calls
Run /stationalert status, which reports which surface each
board holds. This should not happen; if it does, it is worth a Discord
message.
Placements do not survive a restart
The data/ folder has to exist. The console says so on every
save when it is missing.
A station I edited keeps reverting
That id is in config.lua as well as
data/stations.json. Config wins by design, and the console names
the ids that collided.
The apparatus checklist is empty
The list comes from the feed. Open the builder once with the demo provider running and it fills in. On a brand new server the builder asks for a feed snapshot when it opens, so this should resolve itself.
A replace-mode board shows a rectangle instead of the television
The texture could not be claimed — wrong names, the model is not nearby, or another board already claimed that model. The fallback is working as intended. Check the console, which names the txd and txn it tried.
The builder says VIEW ONLY and the buttons do nothing
You do not have the ace. Placing boards, creating stations and adding
devices all need sals_kewlstationalert.admin, and
nothing grants it by default.
Nothing needs restarting. In the server console:
stationalert ace # who is online, and who has it
stationalert ace 1 # grant it to player 1
Then press RE-CHECK on the banner in the panel. The grant is
live but not saved; the command prints the server.cfg line that
makes it permanent.
Being a txAdmin admin is not enough on its own, and an ace
allowed for group.admin does nothing if you are not in
group.admin — see
Granting the ace.
The panel opens at all because Config.Access.Jobs lets a fire
or EMS job change what an existing board shows without being an
admin. Building is a different act and needs the ace.
Click the VIEW ONLY chip in the panel and it says all of this, with the line to copy. The server console also says it once per player, naming the ace — so the owner sees it where they are already looking.
The CAD toned a station and nothing happened here
Read the server console: an unmatched tone-out is refused out loud and
names the station and the slug it arrived with. Either give that house the
same id as the CAD's slug, or add it to
Config.Feed.Cad.Stations — see
Which house did the CAD mean.
If the console says nothing at all, check
Config.Feed.Cad.AcceptToneOuts and that
Config.Feed.Provider is 'cad'.
The CAD toned one station and the WRONG house went off
A slug collision: the CAD's slug for one house matches the id of a different house here, and both sides think they agree. Compare the slugs in the CAD's owner panel against the ids on your STATIONS tab.
If MatchByName is switched on, turn it off and use the slug or
the map instead — two houses sharing a name is the other way this
happens.
The board is empty and the CAD is definitely running
Run stationalert status. The cad line says
whether the link is up and whether the board is arriving. If the console
mentions an export that does not exist, the CAD bridge is older than this
resource expects — update it, or set
Config.Feed.Provider = 'demo' until you do.
I toned a house and nothing happened
It has no devices. Open /stationalert, go to DEVICES and pick
that house: the panel says in amber what is missing. QUICK FIT
fixes it in about ninety seconds.
stationalert status says the same thing in a server console,
and the export returns err_no_devices for the same reason.
err_already_toned
One alert per station per call, permanently, for the life of that call. A
deliberate retone is /stationalert tone again, which is a new alert
and says RETONE on the board.
The lights work but there is no sound
First: does that house have a speaker? The tones, the ramp and the voice all come out of one, and the DEVICES tab says in amber when there is none. Then press PLAY on the SOUNDS tab — if that is silent too, the problem is the audio engine rather than the house.
The sound is synthesized in the builder's own browser frame. Open the F8
console and look for [sals_kewlstationalert] audio. If you have
edited anything in html/, that is where to look first.
Check /stationvolume too — it is remembered per player, on their
machine.
The whistle is far too quiet at three in the morning
That is night mode and it ships on.
Config.Alerting.NightMode.
The voice says nothing
Expected with no phrase bank and no hosted text to speech. The announcement still reaches the board and anybody in quarters. See Voice annunciation.
A bay door will not open
If its driver is native, that MLO's door is probably not
registered with the game door system, and the console says so once. Switch the
device to the pose driver on its own form and record its two
positions.
TEST IT on that door's form opens it without waiting for a call, which is the fastest way to find out which driver your MLO wants.
A bay door will not close
Something is standing under it. It retries, and past the give-up time it is left open and logged. That is deliberate, and it is the safe way round.
Support
Discord: discord.gg/CVWZb6AEwy
When reporting something, the single most useful thing you can paste is the
output of stationalert status from the server console, plus
anything the console said with [sals_kewlstationalert] in front of
it.
If you have found working texture names for a station MLO that is not in the preset list, send them — they will ship for everybody.