Files
link/tools/scaffolding/README.md
colby 81d3553492 Phase 7: PlayerVendorSale core event + subscriber
The one non-drop-in piece. Player-vendor purchases raise no EventSink, so the
sale is invisible to subscription. Two git-format core patches add a
PlayerVendorSale event and raise it at the committed sale in
PlayerVendorBuyGump.OnResponse (right after HoldGold +=), where buyer, vendor
owner, item, price, and commission are all in scope. The subscriber
BridgeVendorSale emits vendor.sale.

All three are a coupled unit. The subscriber references PlayerVendorSaleEventArgs,
which does not exist until the EventSink patch is applied, so it lives in patches/
not overlay/ -- shipping it in overlay would break the build on any unpatched
install. patches/README.md documents applying the unit; both patches verified
with git apply --check against stock ServUO 57.4. This is the first phase that
rebuilds the core (ServUO.exe), not just Scripts.dll.

vendor.sale carries buyer and vendor-owner accounts, both present and distinct,
which is the pair that flags gold-laundering when they match -- richer than the
ownerless NPC ValidVendor* events, and on a committed sale rather than a
validation stage.

Verified with a probe firing the event on real seeded-vendor data: vendor.sale
emitted with buyerAcct=seed_001, ownerAcct=seed_000, Longsword, price 69819. The
probe proves the event, args, subscriber, and payload; the literal gump call site
firing on a real purchase needs a live buyer with a NetState and is confirmed by
an in-game buy. Evidence in docs/PLAN.md §17.

This completes every phase on the ServUO side. Phases 0-6 are drop-in (overlay/);
7 is patches/. Cheat signals are folded into existing streams (fastwalk, audit,
vendor.sale), not a separate phase. Remaining work is the Rust sidecar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:46:07 -05:00

5.5 KiB
Raw Blame History

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.
BridgeCrierProbe.cs Scripts/Custom/BridgeCrierProbe.cs Logs the global town-crier entry list every 3s so towncrier.add / remove can be seen landing in game state. Flag: CrierProbeOnStart.
BridgeVendorSaleProbe.cs Scripts/Custom/BridgeVendorSaleProbe.cs Fires PlayerVendorSale (Phase 7) with real seeded-vendor data so vendor.sale can be verified without a live buy. Requires the Phase 7 patches applied. Flag: VendorSaleProbeOnStart.

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.CanEquip rejects when from.Str < strReq (PlateChest needs 95). A rejected EquipItem leaves the item parentless, and the Cleanup pass later deletes it en masse. The seeder gives characters Str 100125 and deletes any item whose equip is refused, rather than orphaning it.
  • VendorItem.Price is get-only and PlayerVendor.SetVendorItem is private. Dropping an item into a vendor's pack fires OnSubItemAdded, which registers the item at the default price of 999. The seeder reaches SetVendorItem by reflection to set a real price. Acceptable in throwaway scaffolding; do not do this in the plugin.
  • Houses only decay when condemned. BaseHouse.CanDecay is true only for DecayType.Condemned or ManualRefresh. An active owner's newest house is AutoRefresh and never decays. The seeder backdates 18 accounts past Account.InactiveDuration (180 days) to condemn them, then forces stages with SetDynamicDecay — not by backdating LastRefreshed, because DynamicDecay.Enabled is true on this expansion and GetOldDecayLevel is 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 24× for a fully-kitted character.