docs(link): the player-vendor marketplace (Protocol 3.0 §8)
Documents order 5b across the four repos, and records what building it changed about §8 as designed. - NEW website/MARKETPLACE.md — the operator guide: what the pages must say out loud and why, the privacy contract (the player's in-game Vendor Search toggle wins, and no admin setting overrides it), the Bridge.cfg knobs and how they trade against each other, and the measured sweep costs. - INTEGRATION.md — catalog entry for vendor.listing / vendor.listing.remove with its six consumer gotchas, and the GET /market REST section (the sidecar's only paged read, and why it orders by serial rather than shop name). - BACKEND_DESIGN.md — shard_vendors / shard_vendor_items, the routes, and the marketplace search as the only rate-limited public read. - SHARD_VISIBILITY.md — why the market's fields default to Everyone (the in-game gump already shows exactly that set), why location is one setting covering four things, and why hiding the owner name without the owner id achieves nothing. - PLAN.md — the amortized round-robin as the one sweep pattern the bridge did not previously have, and an update to §7's cliloc note: pushing name resolution to the plugin was never an option, because ServUO cannot read a modern client's compressed cliloc files either. - v3.md §8.8 — the four things the build settled differently, chief among them that §8.1's FLAT location payload would have made Part A's pre-wired market.location rule inert, exactly like the characterName miss one part earlier. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
79
link/v3.md
79
link/v3.md
@@ -15,8 +15,8 @@ Each part is marked off here as it lands on `edge`. §9 carries the same state p
|
||||
| 2 | **B/1** — `world.ruleset` (§5) | ✅ **Done** | servuo-plugins [#3](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/3), link [#17](https://gitea.whitlocktech.com/RunicGateway/link/pulls/17), website [#111](https://gitea.whitlocktech.com/RunicGateway/website/pulls/111), docs [#66](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/66) |
|
||||
| 3 | **C** — spawn atlas (§6) | ✅ **Done** | website [#112](https://gitea.whitlocktech.com/RunicGateway/website/pulls/112) (parsers + CLI + tables) + [#113](https://gitea.whitlocktech.com/RunicGateway/website/pulls/113) (API + pages + admin panel), docs [#67](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/67) + [#68](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/68) |
|
||||
| 4 | **B/2** — `points.board` (§7) | ✅ **Done** | servuo-plugins [#4](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/4), link [#18](https://gitea.whitlocktech.com/RunicGateway/link/pulls/18), website [#114](https://gitea.whitlocktech.com/RunicGateway/website/pulls/114), docs [#69](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/69) |
|
||||
| 5a | **B/3 dependency** — cliloc table (§8.6) | 🟨 In review | website [#115](https://gitea.whitlocktech.com/RunicGateway/website/pulls/115), docs [#70](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/70) |
|
||||
| 5b | **B/3** — `vendor.listing` (§8) | ⬜ Not started | — |
|
||||
| 5a | **B/3 dependency** — cliloc table (§8.6) | ✅ **Done** | website [#115](https://gitea.whitlocktech.com/RunicGateway/website/pulls/115), docs [#70](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/70) |
|
||||
| 5b | **B/3** — `vendor.listing` (§8) | 🟨 In review | servuo-plugins [#5](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/5), link [#19](https://gitea.whitlocktech.com/RunicGateway/link/pulls/19), website [#116](https://gitea.whitlocktech.com/RunicGateway/website/pulls/116), docs [#71](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/71) |
|
||||
| 6 | **Cutover** — `PROTOCOL_VERSION` 2→3 (§4) | ⬜ Not started | — |
|
||||
|
||||
Order 5 split in two once §8.6's cliloc dependency turned out to be a client-format problem rather
|
||||
@@ -639,7 +639,7 @@ if it is renamed back.
|
||||
|
||||
---
|
||||
|
||||
## 8. Part B/3 — `vendor.listing`
|
||||
## 8. Part B/3 — `vendor.listing` 🟨 In review
|
||||
|
||||
### 8.1 It cannot be an RPC, and this is load-bearing
|
||||
|
||||
@@ -807,6 +807,75 @@ and text paths converge on identical content.
|
||||
driven by `staleAt` (the oldest `shard_vendors.updated_at`). The round-robin sweep means data is
|
||||
inherently up to one full cycle old, and the UI must say so.
|
||||
|
||||
Shipped with a second page, `routes/public/MarketVendor.jsx` at `/site/market/vendors/:serial` —
|
||||
where a search result points. It is the only surface that can render the two states the result list
|
||||
cannot: a `truncated` shop (*"showing 250 of 3,104 — this shop holds more than the shard
|
||||
publishes"*) and a `location` an admin has gated away, which is a real answer rather than an empty
|
||||
coordinate.
|
||||
|
||||
### 8.8 What the build changed
|
||||
|
||||
Four things the implementation settled differently from §8 as written, all of them found by building
|
||||
against the live shard.
|
||||
|
||||
**1. `location` is a nested object, not flat `map`/`x`/`y`/`region`.** §8.1's payload sketch had them
|
||||
flat, and it would have made `market.location` — a rule Part A pre-wired — **inert**, exactly like
|
||||
the `characterName` miss §7.5 records: `projectValue` matches literal JSON keys, so there is no
|
||||
`location` key for the rule to match. Flat keys would have needed five rules that could drift apart.
|
||||
Nesting makes one rule hide the facet, the coordinates, the region and the house together, on the
|
||||
live frame and the stored read model alike, because both now spell it the same way.
|
||||
|
||||
The other pre-wired rule, `market.ownerName`, checked out — it is a real key on the frame. Owner is
|
||||
written as flat `ownerSerial`/`ownerName` rather than through `BridgeJson.Actor`, which would add
|
||||
`acct` and `webId`; same argument `points.board` makes. `ownerSerial` was **added** to the
|
||||
configurable fields alongside `ownerName`, because an admin who hides the owner's name and leaves a
|
||||
serial every other board resolves back to that name has not hidden anything.
|
||||
|
||||
**2. The per-vendor diff signature is the full listing set, not §8.3's `count | Σ(serial ^ price)`.**
|
||||
That hash collides on the single most common change a shop makes: two items swapping prices, which
|
||||
is what re-pricing looks like. The signature is built over the same buffer the frame is written
|
||||
from, in the same order, so a match really does mean an identical frame.
|
||||
|
||||
**3. There is no `payload` column on `shard_vendors`.** §8.5 implied the board pattern (whole frame
|
||||
in JSON, columns hoisted for display). It does not apply here: the items ARE the searchable rows, so
|
||||
they are normalized into `shard_vendor_items` and there is nothing left worth duplicating. The
|
||||
sidecar keeps the whole blob, because outage resilience is its job and search is not.
|
||||
|
||||
**4. Sweep cost is reported, and a slow tick warns.** The batch cap is a *claim* about per-tick cost,
|
||||
and an operator tuning `MarketSweepBatch` was otherwise tuning blind. `[bridge status` now carries
|
||||
`lastMs`/`maxMs`, and a tick over 50 ms prints a rate-limited warning naming the knob.
|
||||
|
||||
Measured on the live shard (27 vendors × 40 listings, 209k items / 43k mobiles):
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| First tick — 25 vendors emitted cold | **15.4 ms** |
|
||||
| Second tick — the remaining 2 | **3.4 ms** |
|
||||
| Steady state — nothing changed | **0.3 ms** |
|
||||
| Website `/market` search over 1,040 listings | 1,040 total, names resolved |
|
||||
| Cliloc re-resolution pass over 1,040 rows | **50 ms** |
|
||||
|
||||
The diff is what makes the steady state ~free; the batch cap is what bounds the cold case. Note the
|
||||
arithmetic the warning exists for: at the default cap of 250 listings, a batch of 25 **full** shops
|
||||
is 6,250 items ≈ 95 ms — over budget. Real shops hold tens, which is why 25 is the default, but a
|
||||
shard of commodity resellers should lower the batch, and now it will be told to.
|
||||
|
||||
Two smaller things worth not rediscovering:
|
||||
|
||||
- **`BridgeJson.Escape` takes a NON-NULL string** — it dereferences `value.Length` immediately — and
|
||||
`BridgeJson.Str` writes its own `,"key":` prefix, so neither serves a value inside a hand-built
|
||||
object. Nearly everything this frame writes is legitimately null (an item's plain `Name` is null
|
||||
for almost every item; a vendor in the street has no house), so that is the common path, not an
|
||||
edge case. `BridgeMarket.Text()` is the two-line writer that was missing.
|
||||
- **The ServUO console writes in the OS code page**, so an em dash in a `Console.WriteLine` renders
|
||||
as `???` in the log an operator would paste into an issue. Bridge console output is ASCII.
|
||||
|
||||
Search-side, one thing the site had to fix rather than inherit: `%` and `_` in a user's query are
|
||||
**LIKE** metacharacters, not SQL ones, so parameterization does not neutralize them — a search for
|
||||
`%` would otherwise match every listing on the shard. `shardMarket.db.js` escapes them. (The atlas's
|
||||
`LIKE` searches predate this and have the same shape over a much smaller table; worth a follow-up,
|
||||
not a blocker here.)
|
||||
|
||||
---
|
||||
|
||||
## 9. Sequencing
|
||||
@@ -817,8 +886,8 @@ inherently up to one full cycle old, and the UI must say so.
|
||||
| 2 | **B/1** — `world.ruleset` (§5) | all four | new kind | ✅ Done |
|
||||
| 3 | **C** — spawn atlas (§6) | website, docs | none | ✅ Done |
|
||||
| 4 | **B/2** — `points.board` (§7) | all four | new kind + `char.profile` field | ✅ Done |
|
||||
| 5a | **B/3 dependency** — cliloc table (§8.6) | website, docs | none | 🟨 In review |
|
||||
| 5b | **B/3** — `vendor.listing` (§8) | all four | new kinds | ⬜ |
|
||||
| 5a | **B/3 dependency** — cliloc table (§8.6) | website, docs | none | ✅ Done |
|
||||
| 5b | **B/3** — `vendor.listing` (§8) | all four | new kinds | 🟨 In review |
|
||||
| 6 | **Cutover** — `PROTOCOL_VERSION` 2→3, `edge` → `main` | all four | the bump | ⬜ |
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user