feat(bridge): publish the player-vendor market index as vendor.listing
Protocol 3.0 §8. Every player vendor's shop name, owner, location and priced inventory, so the website can offer the search the in-game Vendor Search gump offers — from outside the game, and honouring the same per-player opt-out. It cannot be an RPC. rpc.rs correlates a reply on the FIRST frame carrying a matching reqId, so a chunked reply sharing one reqId would deliver chunk 1 to the HTTP caller and leak chunks 2..N onto the broadcast feed; a whole-world snapshot would not fit in one frame inside the 10 s timeout either. So it is a diff sweep on the broadcast stream, one authoritative frame per vendor. The one genuinely new pattern here is an amortized round-robin: every other sweep walks its whole collection per tick, which is fine for tens of houses and is not fine for a world of shops whose inventories recurse into containers. MarketSweepBatch (25) vendors are inventoried per tick from a persistent cursor, so per-tick cost is bounded by the batch rather than by world size. VendorSearch.GetItemName is never called: it builds an ObjectPropertyList, serialises it and byte-parses the packet per item. The frame carries itemId, hue, amount, price, the plain item.Name field and item.LabelNumber; the website resolves names against its own cliloc table. (It would not work anyway — every current client ships its cliloc files compressed and ServUO's Ultima.StringList cannot read them, so the in-game gump has the same gap.) Measured on the live shard (27 vendors x 40 listings, 209k items / 43k mobiles): 15.4 ms for the first cold tick of 25 vendors, 3.4 ms for the next, 0.3 ms in steady state. `[bridge status` now reports lastMs/maxMs and a tick over 50 ms warns, naming the knob — the batch cap is a claim about that number and an operator tuning it was otherwise tuning blind. - location is ONE nested object, not flat map/x/y/region, so the website's single market.location visibility rule can hide a vendor's whereabouts on both the live frame and the stored read model. Flat keys would need five rules. - Owner is flat ownerSerial/ownerName, never BridgeJson.Actor, which would add acct and webId. Same argument points.board makes. - pv.VendorSearch is honoured, so a shop hidden in game is hidden on the site; the seen-set removal then emits vendor.listing.remove. - Container-priced items carry child:true, exactly as DoSearch reports them. - Over MarketMaxListings (250) the frame says truncated and carries the real total, so the site shows "250 of 3,104" rather than a partial shop as complete. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -166,6 +166,7 @@ namespace Server.Custom.Bridge
|
||||
BridgePresence.Rearm();
|
||||
BridgeHousing.Rearm();
|
||||
BridgePoints.Rearm();
|
||||
BridgeMarket.Rearm();
|
||||
// Not a sweep, so it has nothing to re-arm — but an operator who just edited a
|
||||
// .cfg wants the change on the site now, not after a shard restart.
|
||||
BridgeRuleset.Emit();
|
||||
@@ -186,6 +187,7 @@ namespace Server.Custom.Bridge
|
||||
BridgePresence.SweepOnce();
|
||||
BridgeHousing.SweepOnce();
|
||||
BridgePoints.SweepOnce();
|
||||
BridgeMarket.SweepOnce();
|
||||
e.Mobile.SendMessage("Bridge: ran one sweep of each stream.");
|
||||
e.Mobile.SendMessage("Bridge: {0}", BridgeSweeps.Status());
|
||||
e.Mobile.SendMessage("Bridge: {0}", BridgeChamps.Status());
|
||||
@@ -194,6 +196,7 @@ namespace Server.Custom.Bridge
|
||||
e.Mobile.SendMessage("Bridge: {0}", BridgePresence.Status());
|
||||
e.Mobile.SendMessage("Bridge: {0}", BridgeHousing.Status());
|
||||
e.Mobile.SendMessage("Bridge: {0}", BridgePoints.Status());
|
||||
e.Mobile.SendMessage("Bridge: {0}", BridgeMarket.Status());
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -209,6 +212,7 @@ namespace Server.Custom.Bridge
|
||||
e.Mobile.SendMessage("Bridge: {0}", BridgePresence.Status());
|
||||
e.Mobile.SendMessage("Bridge: {0}", BridgeHousing.Status());
|
||||
e.Mobile.SendMessage("Bridge: {0}", BridgePoints.Status());
|
||||
e.Mobile.SendMessage("Bridge: {0}", BridgeMarket.Status());
|
||||
e.Mobile.SendMessage("Bridge: {0}", BridgePages.Status());
|
||||
e.Mobile.SendMessage("Bridge: {0}", BridgeRuleset.Status());
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user