diff --git a/link/PLAN.md b/link/PLAN.md index e886427..cefc30d 100644 --- a/link/PLAN.md +++ b/link/PLAN.md @@ -309,6 +309,7 @@ Counts in `hello` are a live snapshot taken on the Core thread, not a cached val 4. ~~**Request/response.**~~ **Done.** `BridgeProfile` + `BridgeRequests`: `char.profile` (by account+slot or serial), `account.roster`, `vendor.snapshot`, `bridge.error`. Evidence in §14. Sidecar should cache profiles and rate-limit requests. 5. ~~**`[link` account linking.**~~ **Done.** `BridgeAccountLink`: `[link` → one-time code → `link.confirm` → `WebsiteUserId` tag, persisted to `accounts.xml`. `mob.login` carries `webId`. Evidence in §15. 6. ~~**Town-crier inbound.**~~ **Done.** `BridgeTownCrier`: `towncrier.add` / `remove` into `GlobalTownCrierEntryList`, with abuse caps. Evidence in §16. +7. ~~**Core edit: `PlayerVendorSale`.**~~ **Done.** Two core patches + `BridgeVendorSale` subscriber → `vendor.sale` with buyer + owner + price + commission. Evidence in §17. 5. **`[link` account linking.** `CommandSystem.Register("link", AccessLevel.Player, …)`, one-time short-TTL codes in a main-thread dict, `Account.SetTag("WebsiteUserId", id)` — persists to `accounts.xml` for free. Loopback-only is the trust boundary; add a shared secret if the sidecar is ever exposed. 6. **Town-crier inbound.** `GlobalTownCrierEntryList.Instance.AddEntry(lines, duration)` (`Scripts/Mobiles/NPCs/TownCrier.cs:96`), marshaled to the Core thread. Cap line count/length and active entries. 7. **Core edit: `PlayerVendorSale`** (§6). Then the cheat-detection feed. @@ -350,6 +351,28 @@ Two defects were found this way and fixed: --- +## 17. Phase 7 acceptance + +The one non-drop-in piece. Two `git`-format core patches (`patches/playervendor-sale-*.patch`) add a `PlayerVendorSale` EventSink event and raise it at the committed sale in `PlayerVendorBuyGump.OnResponse` (right after `HoldGold +=`). The subscriber `patches/BridgeVendorSale.cs` emits `vendor.sale`. All three are a coupled unit — the subscriber references a type the patch creates, so it lives in `patches/`, not `overlay/`. + +Both patches verified with `git apply --check` against stock ServUO 57.4. Applying them rebuilds the **core** (`ServUO.exe`), not just `Scripts.dll` — the first phase to do so. + +Verified end to end with a probe that fired the event using **real seeded-vendor data**: + +```json +{"kind":"vendor.sale","committed":true, + "buyerSerial":"0x1F8","buyerAcct":"seed_001", + "ownerSerial":"0x1F5","ownerAcct":"seed_000", + "vendorSerial":"0x2C0","itemSerial":"0x4001440F","itemType":"Longsword", + "itemId":3937,"amount":1,"price":69819,"commission":0} +``` + +Both **buyer and owner accounts are present and distinct** — the pair that flags gold-laundering when they match, and the reason this event beats the ownerless NPC `ValidVendor*` events. + +**Test boundary, stated honestly:** the probe proves the patched event, its args, the subscriber, and the payload. It does **not** exercise the literal call site in `OnResponse` firing on a real purchase — that needs a live buyer with a `NetState` at a vendor, which cannot be faked. That one line is at the verified committed-sale point; the gold-standard confirmation is an in-game buy from a player vendor (buy from a seeded vendor and watch for `vendor.sale committed:true`). + +--- + ## 16. Phase 6 acceptance `BridgeTownCrier.cs` handles inbound `towncrier.add` / `towncrier.remove`, pushing website news into `GlobalTownCrierEntryList` on the Core thread. Caps (line count, line length, active-entry count, duration) are enforced before touching the shared list — defense in depth on top of the loopback trust boundary.