The bridge
The bridge exists to answer one question safely: how does a private game server’s live state reach a public website?
The answer is a sidecar — a small service that owns the connection to the game and the durable copy of what the game said. It is not optional, and the reasons are worth understanding before you build one for another game.
The shape
Section titled “The shape” ServUO shard ──dials out──▶ uo-link sidecar ──HTTP + WS──▶ website (C# plugin) 127.0.0.1:7788 (Rust) bearer + version (module) newline JSON ▲ │ └──────── the game opens NO port ──────┘Three properties fall out of that diagram, and each is a rule rather than an implementation detail.
1. The game dials out
Section titled “1. The game dials out”The sidecar is the listener. The game connects to it. The shard opens no port at all, and nothing on the internet can reach it even in principle.
This inverts the intuitive design — you would expect the thing with the data to serve it — and the inversion is the whole security argument. Only the sidecar is exposed, and only the website’s backend talks to the sidecar.
The transport is deliberately boring: newline-delimited JSON, one object per line, over loopback TCP.
2. A wedged sidecar must never stall the game
Section titled “2. A wedged sidecar must never stall the game”This is the constraint the plugin is built around.
On the C# side, Emit() enqueues onto a bounded, drop-oldest queue and returns
immediately. It never touches the socket from the game’s core thread. Every world read
happens on the core thread; a dedicated writer thread drains the queue.
Inbound commands get the mirror rule: every inbound handler marshals to the core thread before touching world state.
3. The sidecar persists before it forwards
Section titled “3. The sidecar persists before it forwards”The sidecar owns a durable store. It is not a proxy that translates and forgets — if the website is down, the game’s events are still recorded, and a reconnecting website catches up.
This is what “a thin sidecar” means in the Integration Kit: thin in logic, not thin in responsibility. The sidecar is a dumb forwarder — it makes no access-control decisions and holds no policy. Access control and the admin-toggleable visibility scope live on the website, where an administrator can see and change them.
Two ways in
Section titled “Two ways in”Live events arrive over an outbound WebSocket and are routed by the module’s ingest dispatcher. Kinds are handled differently by nature: state-changing kinds update tables, notable kinds append to an events log, and high-frequency kinds only update state rather than accumulating history.
Point-in-time reads and commands go over REST, through a client that never throws.
Every call carries Authorization: Bearer <token> and an X-UOLink-Version header. A
protocol mismatch fails fast with 409 rather than being mis-parsed — see Protocol
versions.
What the shard can say
Section titled “What the shard can say”The catalog spans sessions and identity, character state, economy and commerce, housing and IDOC, combat and PvP, progression, cheat detection and staff audit, and server lifecycle. A representative line looks like:
{"t":1752,"kind":"vendor.sale", "buyer":{"serial":"0x1A2B","acct":"PerryAdimn"}, "owner":{"serial":"0x33C1","acct":"Feng"}, "item":{"serial":"0x4001A2","type":"Longsword","amount":1}, "price":75000,"commission":3750}The full catalog is the Shard event catalog.
Two design details worth stealing
Section titled “Two design details worth stealing”server.hello is per-connection, not per-boot. The sidecar restarts independently of
the game, so anything it needs up front must be re-sent on every connect. An earlier
draft emitted a “started” event once at boot; a sidecar that came up second never received
it and had no idea which shard it was attached to.
It carries a bootId — a GUID generated at server start, stable across sidecar reconnects
and changed on every game restart. That is how the sidecar tells “I reconnected” (keep
cached state) from “the game restarted” (discard it).
Rosters are sets, not signatures. Guild membership is compared as a set rather than folded into a checksum, because a sum can collide: one member joining and another leaving between two sweeps offset each other, and the guild reads as unchanged. A set can also be differenced, which is what makes per-member leave events possible for a game that raises no event for leaving.
On a guild’s first sweep there is no prior set, so nothing is reported as leaving — an unknown roster becoming known is not 155 people leaving at once.
Building one for another game
Section titled “Building one for another game”The bridge is not UO-specific in shape, only in vocabulary. Chapters 3 and 4 of the Integration Kit cover the sidecar and the game-side plugin, and they are the two parts where the mistakes are most expensive.
Canonical documents
Section titled “Canonical documents”link/PLAN.md
§5 and §7 are the data catalog and the wire protocol;
link/INTEGRATION.md
is the integration guide. Both are normative; this page is not.