Channel and scheduler contract
Dispatch is a server-authoritative local call scheduler. It selects and randomizes MP3 clips, publishes lore-friendly metadata and shared timestamps, and leaves playback to Radio or another consumer.
| Channel ID | Intended role | Included clips | Default gap |
|---|---|---|---|
| police | State and police dispatch | 100 | 30-60 seconds |
| fire | Fire response | 100 | 30-60 seconds |
| ems | Medical dispatch | 100 | 30-60 seconds |
| service | City and utility service | 100 | 30-60 seconds |
| air | Aviation operations | 100 | 30-60 seconds |
| marine | Harbor and marine operations | 100 | 30-60 seconds |
initial delay
-> choose a randomized clip
-> publish URL, revision, start time, duration, and metadata
-> clip expires
-> randomized silent gap
-> choose the next clipEach enabled channel owns an independent shuffled deck. Every valid clip is used once before rebuilding the deck; recent-history entries are deferred at the boundary and immediate repeats are avoided.
The expired descriptor remains available during the silent gap. A consumer must compare offset with duration: it can keep the scanner healthy and suppress native chatter without replaying the expired call or falling back to a URL between transmissions.
Installation and startup verification
- Keep the exact san_andreas_dispatch folder name.
- Dispatch itself has no framework, database, or audio-provider dependency.
- Keep matching MP3 files under audio/<channel> and catalog entries in shared/catalog.lua.
- Use a consumer such as San Andreas Radio to select recipients and render the descriptor.
- Start Dispatch before Radio so the first scanner snapshot can use local programming immediately.
ensure san_andreas_sound
ensure san_andreas_dispatch
ensure san_andreas_station_pack
ensure san_andreas_radio- Start Dispatch and watch the server console for catalog errors.
- Start Radio and enter a vehicle mapped to each intended channel.
- Confirm the scanner HUD shows the correct channel in STANDBY.
- Wait for a scheduled call and confirm the state changes to RECEIVING.
- Confirm system, talkgroup, destination, and frequency are lore-friendly.
- Use a second client and confirm both hear the same call at the same offset.
Global and per-channel configuration
| Key | Type | Default | Required | Description |
|---|---|---|---|---|
Config.Debug | boolean | false | — | Adds scheduler and validation logging. |
Config.InitialDelayMs | number | 1000 | — | Delay before channels begin after resource start. |
Config.NoRepeatHistory | number | 6 | — | Recent clips deferred when a shuffled deck is rebuilt. |
Config.DefaultGap.MinMs | number | 30000 | — | Default minimum quiet period after a call. |
Config.DefaultGap.MaxMs | number | 60000 | — | Default maximum quiet period after a call. |
Config.Channels.<id>.Enabled | boolean | true | — | Stops scheduling while retaining files and catalog when false. |
Config.Channels.<id>.Label | string | — | Yes | Server-side full channel label. |
Config.Channels.<id>.MinGapMs / MaxGapMs | number | — | — | Optional per-channel values that override the default gap. |
| PublicMetadata field | Scanner display role |
|---|---|
| Label | Main channel type, such as Fire Dispatch. |
| Talkgroup | Compact lore-friendly channel identifier. |
| TalkgroupLabel | Agency, destination, or call line. |
| Frequency | Lore-friendly frequency displayed after RX. |
| System | Network or communications-system name. |
Config.Channels.air = {
Enabled = true,
Label = 'San Andreas Air Operations',
PublicMetadata = {
Label = 'Air Traffic Control',
Talkgroup = 'LSIA-TWR',
TalkgroupLabel = 'LSIA Tower',
Frequency = '119.8000',
System = 'San Andreas Aviation Authority'
},
MinGapMs = 30000,
MaxGapMs = 60000
}Public metadata affects player presentation only. It does not replace or erase the source provenance retained in shared/catalog.lua and ATTRIBUTION.md.
Connect Dispatch to Radio
Config.Dispatch = {
Enabled = true,
Resource = 'san_andreas_dispatch',
ProfileChannels = {
emergency = 'police',
fire = 'fire',
ems = 'ems',
service = 'service',
aviation = 'air',
marine = 'marine'
}
}| Radio profile setup | Result |
|---|---|
| Mapped profile with Url = '' | Local Dispatch only; no web fallback if Dispatch is unavailable. |
| Mapped profile with a direct Url | Local Dispatch has priority; the URL is fallback only. |
| Unmapped profile with a direct Url | External stream only. |
| Unmapped profile with Url = '' | No valid scanner audio source. |
The mapping key is a Radio scanner profile ID; its value is a Dispatch channel ID. Dispatch never examines vehicles. Vehicle classes, explicit models, disabled models, scanner power, HUD, provider, native scanner suppression, and exterior behavior belong to Radio.
Model-specific profiles are checked before broad vehicle classes. External scanner URLs belong in Radio configuration, never in the local Dispatch catalog.
Maintain the local call catalog
Treat every replacement as one transaction: media, exact duration, catalog entry, provenance, privacy review, attribution, and acceptance testing must remain aligned.
- Confirm that the candidate may be downloaded, hosted, and distributed for the intended release.
- Listen from beginning to end. Reject private names, real addresses, phone numbers, plates, medical details, or other inappropriate identifying information.
- Trim long silence without cutting words and add very short edge fades when needed.
- For speech, MP3 mono at 48 kHz and 64-96 kbps is a practical target. Use lowercase ASCII filenames without spaces.
- Measure the final encoded file with ffprobe; never reuse a duration measured before trimming or conversion.
- Stage the MP3 and catalog edit outside the active resource, then swap them together.
- Update ATTRIBUTION.md and any private audit record, restart Dispatch, check catalog validation, and verify the affected channel in game.
ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 state-dispatch-078.mp3{
id = 'police_replacement_078',
file = 'audio/police/state-dispatch-078.mp3',
duration = 12.736,
system = 'source-system-id',
talkgroup = 1,
talkgroupLabel = 'Source talkgroup',
description = 'Internal audit description',
recordedAt = '2026-08-01T12:00:00.000Z',
sourcePage = 'https://source.example/system/page',
callId = 'source-call-id'
}- Every top-level catalog key must have a matching Config.Channels entry.
- IDs and file paths must be unique; paths must be normalized, relative, and resolve beneath the resource.
- Every referenced file must exist, be MP3, and be declared by the manifest; no unreferenced media should remain.
- Durations must be positive and match the final encoded file.
- Player-facing lore text comes from Config.Channels.PublicMetadata, not raw catalog provenance.
- Adding clips must not bypass shuffled-deck and no-repeat behavior.
Add a new channel
- Create a disabled Config.Channels entry with appropriate public metadata and gap settings.
- Add the audio/<channel> folder and an aligned Catalog.<channel> table of audited local MP3 entries.
- Add the channel ID to CHANNEL_ORDER in san_andreas_dispatch/server/main.lua for the bundled resource.
- Add Config.Dispatch.ProfileChannels.<profile> = '<channel>' in Radio and create or adapt the matching scanner profile.
- Update automated catalog expectations for IDs, counts, prefixes, metadata, and timing before enabling the channel.
Public server API
GetSnapshot
exportServerReturn a map of every bundled channel ID to its current descriptor or false.
GetSnapshot()- Returns
- read-only channel map
GetChannel
exportServerReturn one current descriptor or false.
GetChannel(channelId)- Returns
- descriptor | false
GetCatalogCounts
exportServerReturn raw catalog entry counts by channel ID.
GetCatalogCounts()- Returns
- number map
san_andreas_dispatch:server:transmissionChanged
eventServerLocal server event emitted when a transmission starts, not when it ends.
(channelId, descriptor)| Descriptor field | Meaning |
|---|---|
| kind / channel / channelLabel | Dispatch identity and public channel label. |
| clipId / revision | Stable catalog item and changing transmission revision. |
| url | Local CFX NUI media URL for the active MP3. |
| duration / startedAt / offset | Exact media length and synchronized timing; offset may exceed duration during the silent gap. |
| loop | Always false for a Dispatch transmission. |
| label / talkgroup / talkgroupLabel / frequency / system | Lore-friendly public scanner metadata. |
Verification, troubleshooting, and privacy
- Enter one eligible vehicle for every enabled profile and confirm correct standby metadata.
- Listen to several calls per channel and verify shuffled order, no immediate boundary repeat, exact stop time, and configured quiet gaps.
- Use two clients on the same channel and compare offsets.
- Test startup fade, pause, vehicle exit/re-entry, deletion, and independent resource restarts.
- Confirm Radio suppresses GTA's native scanner only for intended profiles.
A channel never transmits.
Likely causes
- Channel disabled
- Empty or invalid catalog
- Catalog validation error
Checks
- Check Enabled, GetCatalogCounts, and the server console
- Verify every file/path/duration
Expected result
The channel publishes scheduled descriptors after the initial delay.
Radio uses a fallback URL.
Likely causes
- Dispatch stopped or disabled
- Profile unmapped
- Channel invalid
Checks
- Check resource state and Config.Dispatch
- Match profile and channel spelling
Expected result
The healthy mapped local descriptor has priority.
Calls repeat immediately or play sequentially.
Likely causes
- Library too small for history
- Scheduler deck logic changed
- Consumer bypasses snapshots
Checks
- Compare NoRepeatHistory to clip count
- Restore the randomized deck
- Consume synchronized descriptors
Expected result
All valid clips cycle through a shuffled deck without an immediate boundary repeat.
A call ends early or late.
Likely causes
- Catalog duration differs from the final MP3
Checks
- Measure the installed encoded file
- Update the aligned catalog record
Expected result
Playback stops at the exact catalog duration.
One client hears a different point.
Likely causes
- The consumer ignored startedAt or offset
- The provider did not seek after readiness
Checks
- Inspect the descriptor and consumer seek path
Expected result
Late listeners enter the current shared offset.
Duplicate calls remain after restart.
Likely causes
- Duplicate Radio/Sound instances
- Consumer cleanup failure
Checks
- Run one provider and one Radio
- Destroy only consumer-owned playback on stop
Expected result
Independent restarts leave one clean playback path.