Burnsies HideoutDocsBurnsie.Common

Scanner Audio — ScannerAudio

ScannerAudio plays police-scanner voice lines through LSPDFR — crash-proof by construction. Two failure classes that plagued earlier plugins are engineered out:

  1. Missing audio assets. The classic scanner crash was hardcoding audio tokens that only exist when some other mod shipped them. ScannerAudio enumerates the scanner folder (lspdfr\audio\scanner) on this install at startup, and phrases only ever use tokens whose WAVs actually exist here. Missing tokens are dropped from the phrase, not crashed on.
  2. Dropped lines. LSPDFR drops (does not queue) scanner calls while its audio engine is busy. Lines therefore go through a FIFO queue drained by a fiber that is gated on Functions.GetIsAudioEngineBusy — a line waits for a free engine instead of vanishing.

Usage

var scanner = new ScannerAudio(log);
scanner.Initialize();              // enumerate assets (off-thread; call at duty start)
scanner.Start(fibers, flag);       // start the queue drain fiber; once per duty session

// Compose a phrase from tokens — unavailable ones are dropped automatically:
string phrase = scanner.Compose(
    scanner.FirstAvailable("ATTENTION_ALL_UNITS", "AI_OFFICER_REQUEST_BACKUP"),
    "WE_HAVE", "CRIME_OFFICER_IN_NEED_OF_ASSISTANCE",
    "IN_OR_ON_POSITION",           // placeholder LSPDFR fills with zone audio at play time
    "UNITS_RESPOND_CODE_03");

// Enqueue — null phrases are ignored, so Compose output feeds straight in:
scanner.Enqueue(phrase, playerPosition);   // positional: fills IN_OR_ON_POSITION
scanner.Enqueue(phrase);                   // non-positional variant

// Stand-down / teardown:
scanner.Clear();                   // drop everything still queued

Composition API

MemberBehavior
Initialize()Enumerates the scanner folder on a background thread (a modded install can hold thousands of WAVs — never walk it on the duty-start fiber). Until enumeration completes, composition returns null — text-only mode, the correct failure direction for the first seconds of a session.
HasAnyAssetsFalse ⇒ no scanner audio on this install; run text-only.
HasToken(token)True when the token resolves here: exact WAV match, a numbered variant (TOKEN_01TOKEN_04, how stock vocabulary commonly ships), or a known placeholder.
FirstAvailable(a, b, …)First alternative that resolves, or null — for graceful vocabulary differences between installs.
Compose(tokens…)Joins the tokens that resolve, drops the rest (logged at debug). Returns null when nothing resolves — fall back to a text notification.

Placeholders (currently IN_OR_ON_POSITION) are not files on disk; LSPDFR substitutes zone audio at play time. They always pass validation — never filter them out yourself.

Queue behavior

  • FIFO, bounded to 8 entries — an incident should never build an audio backlog.
  • Lines older than 20 seconds are discarded unplayed: stale distress audio is worse than none (imagine "shots fired" airing a minute after the scene calmed down).
  • The drain fiber re-checks the busy gate every 150 ms; a busy engine leaves the line queued, it does not drop it.
  • Playback failures are warnings, never exceptions.

Text fallback pattern

Always pair scanner audio with a visible notification, and treat audio as the garnish:

string phrase = scanner.Compose("ATTENTION_ALL_UNITS", "OFFICER_IN_NEED_OF_ASSISTANCE");
scanner.Enqueue(phrase, position);                    // may be null — fine, ignored
notifier.Show(Palette.Dispatch("officer needs assistance at " + Palette.Key(area)));

This way an install with no scanner assets (or an unlucky vocabulary) still communicates everything — the feature degrades to text, not to silence-plus-confusion.