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:
- A crashing loop kills the feature → every fiber gets an exception fence.
- Fibers leak across duty toggles and reloads → registry with one-call teardown.
- 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
intervalMsof 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
CancellationFlagcancels (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
OffThreadwork — 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 byGame.TimeScale; a slow-motion effect stretches it. Only use it when you want dilation to stretch your delay.- Timers: use
Game.TickCountorDateTime.UtcNow. NeverGame.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();