Burnsies HideoutDocsBurnsie.Common

Callout Base Class — BurnsieCallout

BurnsieCallout is a hardened base class over LSPDFR's Callout. A scenario author writes only scene logic; the base class guarantees that no exit path can leak — every scene entity/blip lives in an EntityBag and every fiber in a FiberRegistry, both torn down exactly once whether the callout is declined, completed, aborted, or the plugin unloads mid-scene. The LSPDFR lifecycle overrides are sealed and exception-fenced, so a scenario bug can never fault LSPDFR's callout runner.

What you implement

[CalloutInfo("StreetBrawl", CalloutProbability.Medium)]
public class StreetBrawl : BurnsieCallout
{
    protected override string CalloutName => "StreetBrawl";

    protected override bool OnSetup()
    {
        // Choose a location; returning false aborts before the callout is offered.
        Anchor = FindSpotNearPlayer();
        CalloutMessage  = "Street Brawl";
        CalloutPosition = Anchor;
        AddMinimumDistanceCheck(80f, Anchor);
        // play dispatch scanner audio here
        return true;
    }

    protected override bool OnAccepted()
    {
        // Spawn everything into Scene; run logic on Fibers gated by Flag.
        var ped = Scene.Keep(new Ped(model, Anchor, 0f));
        Fibers.StartLoop(CalloutName + ".Logic", 250, Flag, LogicTick);
        EndWhenPlayerLeaves(150f);       // auto-end leash (arms only after arrival)
        return true;                     // false = abort cleanly
    }

    protected override void OnTick()  { /* per-frame logic (optional) */ }
    protected override void OnCleanup() { /* extra teardown before Scene empties (optional) */ }
}

Template methods

MethodWhenContract
OnSetup()Before the callout is offeredSet Anchor, CalloutMessage/position/advisory, play audio, show the area blip. Return false to abort.
OnAccepted()Player acceptedSpawn the scene into Scene, start fibers on Fibers. Return false to abort cleanly.
OnTick()Every frame while runningKeep it cheap. Optional.
OnCleanup()During teardown, before Scene.ReleaseAll()Restore anything you set outside the bag (roads, relationships…). Optional.
DebugState()On demandOne-line live state for diagnosis; base reports name, uptime, anchor. Override to expose phase/counters.

What the base provides

MemberPurpose
Scene (EntityBag)Register every entity/blip; released automatically at teardown
Fibers / FlagCallout-scoped fiber registry + cancellation; cancelled/aborted first at teardown
LogLogger prefixed with the callout name
AnchorThe scene anchor position (you set it in OnSetup)
PlayerConvenience for Game.LocalPlayer.Character — still re-validate after yields
Step("...")Logs a phase transition at INFO — narrates the scenario in the log
Finish("reason")Ends the callout with a logged reason
Active (static)The currently running callout, or null — for diagnostics/console tooling only

Built-in behaviors

Route on accept

On accept, the base drops a GPS-routed blip on Anchor (yellow by convention; RouteColor to change) and clears the route once the player gets within ~35 m — the blip itself stays for the scene. Set RouteOnAccept = false in OnSetup if your callout manages its own arrival blip.

The leash — EndWhenPlayerLeaves(distance)

Auto-ends the callout when the player leaves distance meters from Anchor, with two important subtleties baked in:

  • It arms only after the player has actually arrived (first entered ~60% of the radius). At accept time the player is at dispatch distance — a leash that fires en route would kill every callout instantly.
  • It is suspended while a pursuit is active (Pursuits.IsPursuitActive()), and you can suspend it manually with LeashSuspended = true while a scene-owned chase legitimately ranges far from the anchor.

Player-death guard

If the player dies mid-scene, the callout ends cleanly ("player down") — a universal abort so scenes never keep running over a respawn.

Diagnostics for free

On accept, the base logs the scene summary and starts a 4-second state snapshot loop that writes DebugState() to the debug channel. Debug lines always reach the attached diagnostics file (see Logging) even with a quiet console — a playtest produces a reviewable time series of how each callout unfolded, ending with a final-state line at teardown.

Teardown order (why it's safe)

On every exit path, exactly once:

  1. Flag.Cancel() then Fibers.AbortAll()nothing can re-task, re-pursue or re-notify while cleanup below yields.
  2. OnCleanup() — your restores.
  3. Scene.ReleaseAll() — peds/vehicles dismissed to ambient, blips deleted.

Base lifecycle calls (base.OnCalloutAccepted() etc.) are chained internally — LSPDFR's base methods carry required state plumbing and are never skipped.

Rules

  • Everything you spawn goes through Scene.Keep(...) — creation code should read Scene.Keep(new Ped(...)).
  • Every loop goes through Fibers with Flag — never a bare GameFiber.StartNew loop.
  • Anything you set outside the bag (roads closed, relationship groups, density suppression) is restored in OnCleanup().
  • OnTick runs on LSPDFR's callout fiber — keep it cheap; long work belongs on your own fibers.