BridgeEvents subscribes the streams selected for tracking, economy, and cheat detection: Login/Logout/AccountLogin, AccountGoldChange, ValidVendorPurchase/ Sell, PlacePlayerVendor, SkillGain, FameChange, KarmaChange, QuestComplete, PlayerDeath, PlayerMurdered, OnKilledBy, FastWalk, OnPropertyChanged, Command, and Before/AfterWorldSave. Every handler runs on the Core thread inside the path that raised it, so each is wrapped to never throw, does only Emit (which enqueues and returns), and never mutates the args. Three of these are veto hooks and are read strictly: AccountLogin (Accepted/RejectReason, and a plaintext Password we never emit), FastWalk (Blocked), and the login decision path generally. Testing on the live shard found that SkillGain fires for NPCs, hard: the first boot emitted 115 skill.gain events in four seconds, all spawned creatures grinding Meditation, zero players. That is the general rule here — most "player" events also fire for NPCs — so SkillGain, FameChange, KarmaChange, and OnKilledBy all filter to players on the Core thread before the socket. Gold, fame, karma, and the save boundaries were fired through their real code paths and observed at the stub sidecar; gold.change round-trips the platinum->gold conversion and persists across restarts. Evidence in docs/PLAN.md §12. Adds tools/scaffolding/BridgeEventProbe.cs (never deployed) which triggers those events through real world mutations rather than synthetic Invoke calls. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4.4 KiB
uo-link
ServUO ⇄ Rust sidecar bridge. The shard emits newline-delimited JSON over a loopback TCP socket; the sidecar owns the WebSocket the website consumes.
ServUO plugin (C#, net48) ──loopback TCP, newline-JSON──► Rust sidecar ──WebSocket/JSON──► website
(Core-thread reads) ◄──inbound commands─────────────┘ (owns WS, auth, buffering, fan-out)
The shard never speaks WebSocket. Every world read happens on the Core thread; the socket is touched only by a dedicated writer thread draining a bounded queue.
Layout
| Path | What |
|---|---|
overlay/ |
Mirrors the ServUO server root. Everything here — and only this — copies over an install. |
patches/ |
Unified diffs against stock ServUO for files we must modify rather than add. |
tools/ |
Never deployed. Test scaffolding and anything else that must not reach a server. |
docs/PLAN.md |
Implementation plan, measured performance budget, and the full data catalog. |
docs/RESEARCH.md |
Original source-level research. Partly superseded — see the corrections table in PLAN.md §8. |
docs/SHARD_PREREQS.md |
Repairs the target shard needed before any of this could load. |
deploy.ps1 |
Copies overlay/ into a server root. -Verify diffs instead of writing. |
Anything under overlay/ is authoritative. Do not edit files in the server tree directly — edit here and deploy.
Deploy
.\deploy.ps1 -ServerPath C:\Users\colby\Desktop\servuo -Verify # show what would change
.\deploy.ps1 -ServerPath C:\Users\colby\Desktop\servuo # write
Status
| Phase | State |
|---|---|
0 — build fix (Scripts.csproj) |
done, verified end-to-end |
1 — transport (BridgeLink) |
done, acceptance in docs/PLAN.md §11 |
2 — event streams (BridgeEvents) |
done, acceptance in docs/PLAN.md §12 |
| 3 — sweeps | not started |
| 4 — request/response | not started |
5 — [link account linking |
not started |
| 6 — town-crier inbound | not started |
7 — PlayerVendorSale core event |
not started |
| 8 — cheat signals | not started |
Phase 0 — what it fixes
ScriptCompiler.Compile() runs dotnet build Scripts/Scripts.csproj -c Release, prints the output, and never checks the exit code, then Assembly.LoadFrom("Scripts.dll") and returns true. Because that build passed no Platform, MSBuild defaulted to AnyCPU, and Scripts.csproj gated both OutputPath and DefineConstants on Configuration|Platform == Release|x64. So:
- the DLL landed in
Scripts/bin/Release/while the core loadsScripts.dllfrom the base directory, and TRACE;NEWTIMERS;ServUOwent undefined, so XmlSpawner compiled its non-ServUO branches.
Runtime script compilation therefore had no effect, silently. overlay/Scripts/Scripts.csproj conditions both property groups on Configuration alone.
Server.csproj is deliberately left alone: nothing under Server/ uses those symbols, and giving it OutputPath=..\ would make the boot-time build try to overwrite the running ServUO.exe.
The plugin (Phase 1)
overlay/Scripts/Custom/Bridge/:
| File | Responsibility |
|---|---|
BridgeConfig.cs |
Reads Config/Bridge.cfg in Configure(), before World.Load. |
BridgeJson.cs |
Outbound JSON by hand (Core thread, so no reflection serializer). Inbound via JavaScriptSerializer. |
BridgeLink.cs |
The socket. Link thread owns it; a bounded drop-oldest queue fronts it; a reader thread marshals inbound lines to the Core thread. |
BridgeBoot.cs |
Lifecycle, inbound dispatch, [bridge status|reload|ping]. |
BridgeEvents.cs |
EventSink subscriptions (Phase 2). Read-only, player-filtered, never emits secrets. |
Emit() is called from the Core thread. It enqueues and returns — it never touches the socket, never blocks, never allocates a syscall. A wedged or absent sidecar cannot stall the shard, and that is the property everything else depends on.
Testing
tools/stub_sidecar.ps1 is a loopback listener that logs every line the shard sends. Run it, boot the shard, watch server.hello arrive.
.\tools\stub_sidecar.ps1 -Port 7788 -Log .\sidecar.log
tools/scaffolding/ holds the world seeder and the performance probe. Neither is deployed — deploy.ps1 only copies overlay/. They produced the budget in docs/PLAN.md §1. See tools/scaffolding/README.md.