# 4. The game-side plugin The chapter with the least code and the highest stakes. Everything else in this book fails by showing an operator a broken web page; this part fails by taking the game down while people are playing it. If your game already speaks a remote-control protocol, you may not need any of this — see the end of [chapter 3](03-sidecar.md). If it does not, something has to run inside the game and feed your sidecar, and the rules below are what keep that something from being the reason the server froze. The worked example is `servuo-plugins`, the Ultima Online shard plugin, whose link layer is one file: `overlay/Scripts/Custom/Bridge/BridgeLink.cs`. It is C# against a specific game engine and none of that transfers. The threading contract at the top of it does, entirely. --- ## The one rule: never block the game A game server is a loop. Whatever thread runs the world is the thread that must not stop, and every rule in this chapter is a restatement of that. **Emitting an event must enqueue and return.** It formats nothing expensive, waits on nothing, and touches no socket. In `BridgeLink.cs`, `Emit` is called from the game's own thread, appends a line to a queue, signals a waiting writer, and returns. A sidecar that is slow, wedged, restarting or entirely absent cannot stall the game, because the game never touches the connection. The failure this prevents is not hypothetical, and it is not a small one: a socket write from the world thread against a peer that has stopped reading blocks until the OS buffer drains. That is a frozen game server, caused by a monitoring feature, at exactly the moment something else is already wrong. ## The queue is bounded, and it drops the oldest An unbounded queue in front of an absent consumer is a memory leak with a delay timer on it. So the queue has a cap, and when it is full **the oldest record is dropped and counted**. `Emit` bounds first and enqueues second, so the queue can sit transiently one over the cap and never grows without limit. `BridgeLink` exposes counters — sent, dropped, received, connects, write errors, current depth — and those counters are what an operator debugs from later. Dropping is correct here, and it is worth being explicit about why: **telemetry is worth less than the game's memory.** If your sidecar has been unreachable for ten minutes, the useful thing is the most recent state of the world, not a ten-minute-old backlog delivered before it. Newest-wins is the honest policy, and "stall the game rather than lose an event" is never the trade to make. Where losing events is genuinely unacceptable, the answer is the sidecar's store ([chapter 3](03-sidecar.md)), not a bigger queue inside the game. ## One writer thread owns the socket A dedicated thread drains the queue and owns the connection. It connects, reconnects with backoff, and writes. **A single writer is also what keeps event ordering intact** — with two, the order events reach the sidecar is the order two threads happened to be scheduled in, and you find out from a board that says a player logged out before they logged in. The reconnect loop in `LinkLoop` backs off with a low ceiling — a few seconds, because a loopback reconnect is cheap and a sidecar restart should cost a few seconds of buffering rather than half a minute of blindness. Pick your ceiling from what the connection actually costs, not from a habit borrowed from internet clients. One detail there is subtle enough to be worth stealing: `BridgeLink` tags each connection attempt with an **epoch**, so a reader thread from a previous connection cannot tear down the connection that replaced it. Joining a thread can time out; the stale thread's cleanup then runs against whatever is current. If you write a reconnect loop, write it so a late-arriving cleanup from a dead connection is a no-op. ## Read the world only on the game's thread Inbound is the mirror image. A reader thread parses lines off the socket, and then **hands each one to the game's own thread** to act on — `BridgeLink`'s `Dispatch` does it by scheduling a zero-delay callback on the game's timer, which is the engine's supported way in. The reader itself never touches the world's objects. Two rules fall out and both are absolute: - **Every read of the world happens on the world's thread.** Game engines are overwhelmingly single-threaded about their state, and reading a collection while the loop mutates it is a crash or, worse, a corruption you notice a week later. - **The writer thread only ever sees plain data.** Format your line — a string, a buffer, whatever your wire is — on the game's thread while the objects are safe to read, and hand the finished bytes over. Never hand the writer a live game object to serialise. And an error boundary at the seam: a malformed command from the sidecar must never escape into a game code path. `BridgeLink` wraps the inbound handler and logs anything it throws, because the alternative is an exception unwinding somewhere in the engine's main loop. ## Reconnect, and what to send on connect Your sidecar restarts independently of your game. It comes back with an empty picture, and it cannot ask the game for one without an inbound path you may not have built yet. So **anything the sidecar needs up front is re-sent on every connect, not once at startup.** In `servuo-plugins` that is an explicit event: the link exposes a "connected" hook that runs on the game thread, and each feature area subscribes to it and re-emits its current state — the hello line, the house registry, the guild and governor boards, the market, the ruleset. A sidecar that has just started is therefore fully populated within one connection, with no negotiation. The general form: **for every board your website renders, have exactly one place that can produce its current state, and call it on connect.** If you cannot name that place for some piece of state, your sidecar will eventually be missing it and nobody will know why. ## Events, snapshots, and the state that has neither You will end up emitting two different kinds of thing, and confusing them is a design mistake that shows up as a bad page. **An event** is something that happened, at a time: a player logged in, a house fell, a trade completed. Events are appended and read back as history. **A snapshot** is the current state of a subject: this board's rows, this guild's membership, the published ruleset. Snapshots overwrite; nobody wants the history of a leaderboard's every intermediate ordering. Send both, and be clear at the wire about which a message is — your sidecar's store handles them differently ([chapter 3](03-sidecar.md)), and a snapshot appended as history is a table that grows forever. Then there is the state your engine gives you no hook for at all. Player vitals, decay timers, money supply: nothing fires when they change. `servuo-plugins` polls those on the game's own thread with repeating timers, in `overlay/Scripts/Custom/Bridge/BridgeSweeps.cs`, and the comment worth copying is that this is only acceptable **because the cost was measured**. A full pass of all three sweeps is well under a millisecond at that shard's scale. Measure yours before you add a timer to a game loop, and if a sweep is expensive, sample it — never move it off the game thread. Two practical notes from that file, both general: - **Emit a transition, not a level.** The decay sweep keeps the last known level per house and emits only when one changes, and it takes a silent baseline at startup so a restart does not re-announce every house's current state as news. - **Know when your engine suspends timers.** These do not fire during a world save, so a sweep that would have landed mid-save simply happens a few seconds later. That is fine for all three — but it is fine because someone checked, not by default. ## A checklist for the plugin you are about to write 1. The emit path enqueues and returns. Nothing on the game thread touches a socket. 2. The queue is bounded and drops the oldest, and something counts the drops. 3. One writer thread owns the connection; ordering is therefore intact. 4. Reconnect with a bounded backoff; a stale connection's cleanup cannot affect a newer one. 5. Inbound lines are marshalled onto the game thread before touching the world, and a handler that throws cannot escape into the engine. 6. Every board's current state has exactly one producer, and all of them run on connect. 7. Every world read is on the world's thread; the writer sees only plain data. 8. Anything polled has had its cost measured against a realistic world. If all eight hold, the worst a broken sidecar can do to your game is nothing at all — which is the entire point of the arrangement. --- That is the book. The three parts are a module core loads, a sidecar that owns the game connection and the durable copy of what it said, and a plugin that feeds the sidecar without ever waiting on it. If you got this far and built something, the places you got stuck are the most valuable thing this repo can receive — [tell us][issues], and please say where you left the kit and what you did next. [issues]: https://gitea.whitlocktech.com/RunicGateway/Integration-kit/issues