From edb1fdcbe219c90e1a0f89e2e67d0a1167e04e75 Mon Sep 17 00:00:00 2001 From: colby Date: Fri, 10 Jul 2026 11:34:14 -0500 Subject: [PATCH] Phase 4: character-profile request/response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BridgeProfile builds the read-models the website consumes; BridgeRequests registers the inbound handlers. The sidecar asks, the shard answers on the Core thread (inbound lines are marshaled through Timer.DelayCall before a handler runs), so all of these read live world state safely. - char.request: resolve by serial, or by account + slot, and reply with a full profile (stats, all trained skills, worn equipment with flattened AOS mods, resists). Works for offline characters since a logged-off mobile stays resident until Delete. - account.roster: light per-character summary, offline chars included. - vendor.snapshot: every player vendor owned by an account, with held gold and priced listings. Each request may carry a reqId the reply echoes so the sidecar can correlate. An unresolvable request gets a bridge.error reply rather than silence, so the website can show a real failure instead of hanging. Verified against the real world with a sending stub: all five requests answered, both char lookup paths (account+slot and serial) returning the identical profile, vendor.snapshot returning seed_000's two vendors and 80 listings, and the bad account returning bridge.error. Two real-data findings noted in docs/PLAN.md §14: a GM character can have skill base > cap (the website must not assume otherwise), and the mod-flattening path still wants a genuinely kitted character to exercise against real suffix gear. Adds tools/stub_sidecar_request.ps1 (sends requests) and a hardened tools/stub_sidecar.ps1 (survives reaping/rebind). Co-Authored-By: Claude Opus 4.8 --- link/PLAN.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/link/PLAN.md b/link/PLAN.md index 76fa16d..5c2dc2d 100644 --- a/link/PLAN.md +++ b/link/PLAN.md @@ -306,7 +306,7 @@ Counts in `hello` are a live snapshot taken on the Core thread, not a cached val 1. ~~**Transport.**~~ **Done.** `BridgeLink`: `TcpClient`, link thread + bounded drop-oldest queue, reader thread → `Timer.DelayCall`, reconnect with backoff capped at 5 s. Emits `server.hello` / `server.shutdown` / `server.crashed`, answers `ping` with `pong`. `[bridge status|reload|ping]`. Acceptance evidence in §11. 2. ~~**Cheap event streams.**~~ **Done.** `BridgeEvents` subscribes the streams selected below. All observed on the live shard; evidence in §12. 3. ~~**Sweeps.**~~ **Done.** `BridgeSweeps`: vitals / decay-on-transition / economy, all Core-thread timers, re-armable. Evidence in §13. -4. **Request/response.** `char.profile`, `account.roster`, `vendor.snapshot`. Sidecar caches profiles; rate-limit requests sidecar-side. +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.** `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. @@ -348,6 +348,27 @@ Two defects were found this way and fixed: --- +## 14. Phase 4 acceptance + +`BridgeProfile.cs` builds the read-models; `BridgeRequests.cs` registers the inbound handlers (`char.request`, `account.roster`, `vendor.snapshot`). Each request may carry a `reqId` the reply echoes; an unresolvable request gets a `bridge.error` reply, never silence. + +Verified against the **real world** with a sending stub (`tools/stub_sidecar_request.ps1`), five requests, all answered on the Core thread: + +- `account.roster` for `whitlocktech` → one char, Darrow, slot 0, offline. +- `char.request` by account+slot → full profile: stats, all 58 skills, resists, worn equipment, `reqId` echoed. +- `char.request` by `serial:"0x24C"` → byte-identical profile. Both resolution paths agree. +- `vendor.snapshot` for `seed_000` → its two vendors, held gold, all 40 priced listings each. +- `char.request` for a bogus account → `{"kind":"bridge.error","reqId":"r-bad","reason":"unknown account"}`. + +Two things the real character surfaced that the seeded dummies could not: + +- **`base > cap` is possible.** Darrow (a GM character) reports every skill `base:120, cap:100`. The website must not assume `base <= cap`. The profile reports both faithfully. +- **The mod-flattening path was not exercised against real suffix gear.** Darrow wears starter shirt/pants/shoes with empty `mods`. The flattening code is the same path proven by the Phase 1 timing probe, but a genuinely kitted character (weapon/armor with AOS attributes) would be the honest end-to-end test. Not blocking. + +Offline profiles work: Darrow was logged out and the full sheet still built, because a logged-off mobile stays resident until Delete. + +--- + ## 13. Phase 3 acceptance `BridgeSweeps.cs` runs three repeating Core-thread timers: vitals (`StatSweepSeconds`), house decay (`DecaySweepSeconds`), economy supply (`EconomySweepSeconds`). All re-armable via `[bridge reload`; `[bridge sweepnow` runs one of each on demand; `[bridge status` reports sweep counters.