BridgeAccountLink ties a game account to a website account. [link mints a one-time, 5-minute code from an unambiguous alphabet (no O/0/I/1), holds it in a Core-thread dict keyed to the account, and emits link.request. The website relays the code back through the sidecar as link.confirm; the shard validates, writes the WebsiteUserId account tag, and replies link.ok. A bad or expired code gets link.error. The tag persists to accounts.xml in ServUO's standard <tags> format, read by LoadTags at boot, so a link survives restarts with no new persistence layer. mob.login now carries webId when the account is linked, so the sidecar can attribute a session to a site user without a lookup. Safeguards: one-time codes; only the newest code per account is valid; per-account 30s rate limit against code spam; a 1-minute purge bounds the code table; the websiteUserId is trusted only because the socket is loopback-only. The tag reaches memory on confirm but disk only on the next save — a hard crash between loses it, and the player just re-runs [link. Verified end to end with a smart stub that reads the emitted code and confirms it: link.request -> link.confirm -> link.ok, a bad code -> link.error, and the tag observed in accounts.xml after a save. Evidence in docs/PLAN.md §15. The [link command body is exposed as RequestLink(Mobile) so it can be driven in tests without a client. Adds tools/stub_sidecar_link.ps1 and tools/scaffolding/BridgeLinkProbe.cs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Test scaffolding
Not part of the bridge. Never deployed. deploy.ps1 only copies overlay/, so nothing here reaches a server unless you put it there by hand.
These two scripts produced the measured budget in docs/PLAN.md §1. They are kept because those numbers should be reproducible, and because re-running the probe is the only honest way to check whether a change to the plugin's read path got more expensive.
| File | Server path when testing | What |
|---|---|---|
BridgeSeeder.cs |
Scripts/Custom/BridgeSeeder.cs |
Populates a synthetic world: 50 accounts, 150 characters, 30 houses, 30 player vendors with 40 listings each. |
BridgeProbe.cs |
Scripts/Custom/BridgeProbe.cs |
Times every read the plugin performs, on the Core thread. Read-only. |
BridgeEventProbe.cs |
Scripts/Custom/BridgeEventProbe.cs |
Fires gold/fame/karma/save events through their real code paths so the emit path can be verified without a game client. Mutates the world and saves. Flag: EventProbeOnStart. |
BridgeSweepProbe.cs |
Scripts/Custom/BridgeSweepProbe.cs |
Bumps one seeded house's decay stage after baseline so the decay sweep's transition detection can be observed without waiting a real IDOC stage. Flag: SweepProbeOnStart. Pair with short *SweepSeconds overrides. |
BridgeLinkProbe.cs |
Scripts/Custom/BridgeLinkProbe.cs |
Triggers [link for seed_001 without a client, then saves so the WebsiteUserId tag reaches accounts.xml. Flag: LinkProbeOnStart. Pair with a sidecar that reads the code and sends link.confirm. |
Deploy overwrites Bridge.cfg
deploy.ps1 copies overlay/Config/Bridge.cfg, which deliberately omits the scaffolding flags. So every deploy strips SeedOnStart / EventProbeOnStart / etc. Re-append the flag you need after deploying, or the probe silently does nothing on the next boot. (This bit once during Phase 2 testing.)
Using them
Copy both into Scripts/Custom/, then append the flags to Config/Bridge.cfg:
SeedOnStart=True
CensusOnStart=False
ProbeOnStart=False
Boot once to seed and save, then set SeedOnStart=False. CensusOnStart reports what the loaded world actually contains; ProbeOnStart prints timings two seconds after ServerStarted.
Because Config.Get returns false for a missing key, a server whose Bridge.cfg lacks these keys never runs the scaffolding — even if the .cs files are sitting in Scripts/Custom/. That is the safety net, not an excuse to ship them.
In-game, [seedworld and [unseedworld (Administrator) do the same work on a live shard.
Back up Saves/ first
[seedworld and SeedOnStart write to the live world. Copy Saves/ somewhere outside the repo before running either. Backups/Automatic is rotated by AutoSave.cs and Backups/Temp is deleted outright, so neither is a safe destination.
[unseedworld deletes every seed_* account, which takes their characters and houses with it — but not necessarily their PlayerVendor mobiles. Restoring a backup is the reliable reset.
What the seeder had to work around
Worth knowing before you trust its output:
- Plate needs strength.
BaseArmor.CanEquiprejects whenfrom.Str < strReq(PlateChestneeds 95). A rejectedEquipItemleaves the item parentless, and theCleanuppass later deletes it en masse. The seeder gives charactersStr100–125 and deletes any item whose equip is refused, rather than orphaning it. VendorItem.Priceis get-only andPlayerVendor.SetVendorItemis private. Dropping an item into a vendor's pack firesOnSubItemAdded, which registers the item at the default price of 999. The seeder reachesSetVendorItemby reflection to set a real price. Acceptable in throwaway scaffolding; do not do this in the plugin.- Houses only decay when condemned.
BaseHouse.CanDecayis true only forDecayType.CondemnedorManualRefresh. An active owner's newest house isAutoRefreshand never decays. The seeder backdates 18 accounts pastAccount.InactiveDuration(180 days) to condemn them, then forces stages withSetDynamicDecay— not by backdatingLastRefreshed, becauseDynamicDecay.Enabledis true on this expansion andGetOldDecayLevelis unreachable.
Reference output
Census after a fresh load of the seeded world:
[BridgeSeeder] houses=35
[BridgeSeeder] decay Ageless 13, Slightly 3, Somewhat 7, Fairly 3, Greatly 3, IDOC 6
[BridgeSeeder] seeded chars=150 avgEquipped=8.00 naked=0
[BridgeSeeder] playervendors=30
Probe, best-of-20 on the Core thread:
[BridgeProbe] char.profile 0.069 ms/char 2386 bytes json
[BridgeProbe] vitals sweep 0.223 ms for 150 chars (0.0015 ms/char)
[BridgeProbe] decay sweep 0.007 ms for 35 houses (0.0002 ms/house)
[BridgeProbe] economy sweep 0.001 ms for 51 accounts (supply 110,478,209 gold)
[BridgeProbe] vendor snap 0.343 ms for 30 vendors (1200 listings)
Seeded characters carry 8 items with ~6 mods each and ~12 trained skills. A real endgame character has more of both, so profile cost and payload are a floor — budget 2–4× for a fully-kitted character.