Burnsies HideoutDocsBurnsie.Common

Threading & Fibers — FiberRegistry, CancellationFlag, OffThread

Burnsie.Common.Threading is the backbone every long-running feature in the library sits on. It solves the three chronic threading problems of RPH plugins:

  1. A crashing loop kills the feature → every fiber gets an exception fence.
  2. Fibers leak across duty toggles and reloads → registry with one-call teardown.
  3. Blocking IO on a fiber stalls the game → dedicated background-thread helper.

FiberRegistry — named fibers with exception fences

var flag   = new CancellationFlag();
var fibers = new FiberRegistry(log);

// A monitor loop: body runs every 250 ms of real time until the flag cancels.
fibers.StartLoop("MyPlugin.Watch", 250, flag, () =>
{
    // one iteration; exceptions here are logged and the loop KEEPS RUNNING
});

// A one-shot: event hand-off, finite sequence, etc.
fibers.StartOnce("MyPlugin.OneShot", () =>
{
    // fenced; an exception is logged, never propagated into RPH
});

What StartLoop guarantees:

  • The body runs once per iteration, separated by intervalMs of real time (intervalMs: 0 = yield-only, i.e. once per frame).
  • The loop yields every iteration — it can never starve the game thread.
  • It exits promptly when its CancellationFlag cancels (checked before and after each body run).
  • A throwing iteration is logged and followed by a 1-second back-off, so a persistent fault cannot spam the log at frame rate — and one bad frame cannot kill a monitor.

Teardown (duty end, Finally()):

flag.Cancel();       // cooperative: loops exit at their next check
fibers.AbortAll();   // backstop: aborts anything still alive, clears the registry

Always cancel first, abort second. Prune() exists to drop references to finished fibers opportunistically; it is never required for correctness.

CancellationFlag — cooperative cancellation

A deliberately tiny, thread-safe flag (a mirror of the smallest useful subset of CancellationToken):

var flag = new CancellationFlag();
flag.IsCancelled;   // poll between yields
flag.Cancel();      // idempotent, thread-safe
flag.Reset();       // re-arm for a new session (e.g. next duty start)

Scope your flags deliberately. The common pattern is one duty-wide flag for session-long services, plus short-lived incident-scoped flags for things that must be cancellable independently (see the Backup dispatcher for the canonical example).

OffThread — blocking work off the game thread

GameFibers are cooperatively scheduled on the game thread. Any blocking call — HTTP, file IO, directory enumeration — stalls the entire game for its duration. OffThread.Run moves that work to a background CLR thread:

OffThread.Run(log, "MyPlugin.LoadAssets", () =>
{
    var bytes = File.ReadAllBytes(path);   // fine here; never on a fiber
    // hand results back via thread-safe state; game work still needs a fiber
});
  • Exceptions are logged, never rethrown into the game.
  • The thread is a named background thread (it won't keep the process alive).
  • Never touch game state (natives, entities) from OffThread work — marshal back to a GameFiber for that.

Sleep vs Wait vs timers

  • GameFiber.Sleep(ms) — real time. Use this for pacing.
  • GameFiber.Wait(ms) — scaled by Game.TimeScale; a slow-motion effect stretches it. Only use it when you want dilation to stretch your delay.
  • Timers: use Game.TickCount or DateTime.UtcNow. Never Game.GameTime — it dilates.

Lifecycle pattern

The shape used by every Burnsie plugin:

// duty start
_flag = new CancellationFlag();
_fibers = new FiberRegistry(_log);
notifier.Start(_fibers, _flag);
scanner.Start(_fibers, _flag);
_fibers.StartLoop("MyPlugin.Main", 100, _flag, MainTick);

// duty end / reload / unload — one path, always safe to re-run
_flag?.Cancel();
_fibers?.AbortAll();