fix(bridge): make the market sweep notice a vendor running out of gold

`BridgeMarket.Signature()` diffs shop name, owner, map, coordinates and the
item/price list -- the things a LISTING is made of. Protocol 5 added a `fees`
block to the frame and the change detector never learned about it.

So a vendor quietly running down its gold altered nothing the sweep compared,
emitted no frame, and `uo.vendor.expiring` -- the notification whose entire
subject is a vendor running out of gold -- could fire only by coincidence: when
somebody happened to reprice an item on a shop that was already broke. Proved on
the engagement Phase 11b live rig by setting a vendor's held gold to zero and
watching no frame follow.

The signature carries the DERIVED values, `exempt` and `periodsRemaining`, not
the raw ones. An integer division moves only when the shard's own answer to "is
this vendor in danger" moves; `HoldGold` changes on every sale and `NextPayTime`
on every tick, and keying on either would re-emit a fat listing frame for a shop
whose listings had not changed.

Emit CADENCE, not frame shape: no field added, PROTOCOL_VERSION untouched, and
`overlay.toml` unchanged. The general form is worth carrying forward -- a
sweep-based kind has a change detector, and a field added to the frame but not to
the detector ships correct and arrives never.

Also adds `tools/scaffolding/BridgeRigDriver.cs`: the shard driven from outside
the game over a polled command file. A walk asserts what happened BETWEEN two
steps, so the steps have to be separated by the observer rather than by a
hard-coded delay -- and ServUO's console takes a fixed verb set, so `[p5probe`
cannot be typed at a headless shard at all. Never deployed; `deploy.ps1` copies
only `overlay/`. The README gains the two ServUO facts the walk cost a rebuild
each to learn: a condemned house cannot be refreshed, and only a clean shutdown
emits.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-09-01 07:12:46 -05:00
parent a144c12c46
commit 1dd490b483
3 changed files with 552 additions and 0 deletions

View File

@@ -456,6 +456,20 @@ namespace Server.Custom.Bridge
sb.Append(vendor.Map == null ? "" : vendor.Map.Name).Append('|');
sb.Append(vendor.X).Append(',').Append(vendor.Y).Append('|');
// The FEE STATE, and it belongs here for a reason found on a live rig: a vendor
// quietly running out of gold changes none of the fields above, so without this the
// sweep sees no change, emits nothing, and `uo.vendor.expiring` -- the warning whose
// entire subject is a vendor running out of gold -- can only fire by coincidence,
// when somebody happens to reprice an item on a shop that is already broke.
//
// The DERIVED values, not the raw ones. `periodsRemaining` is an integer division, so
// it moves only when the shard's own answer to "is this vendor in danger" moves --
// near-zero extra frame volume -- while `HoldGold` changes on every sale and
// `NextPayTime` on every tick, which would re-emit a fat listing frame for a shop
// whose listings did not change. Protocol-neutral: the fields already ship in
// `AppendFees`, and this changes only WHEN a frame is sent.
AppendFeeSignature(sb, vendor);
var limit = Math.Min(_items.Count, BridgeConfig.MarketMaxListings);
sb.Append(_items.Count).Append('|');
@@ -624,6 +638,27 @@ namespace Server.Custom.Bridge
/// projection does. Unlike a dynamic-decay house, though, there is no randomness in it:
/// given the current funds it is the exact tick the vendor is destroyed on.
/// </summary>
/// <summary>
/// The fee state as the change-detector sees it: exempt, and how many pay ticks the
/// vendor survives. Kept beside `AppendFees` so the two cannot drift -- a fee field
/// that becomes decision-relevant has to be added in both places, and this comment is
/// where the next person is told so.
/// </summary>
private static void AppendFeeSignature(StringBuilder sb, PlayerVendor vendor)
{
if (vendor == null || vendor.IsCommission)
{
sb.Append("exempt|");
return;
}
int charge = BaseHouse.NewVendorSystem ? vendor.ChargePerRealWorldDay : vendor.ChargePerDay;
int funds = BaseHouse.NewVendorSystem ? vendor.HoldGold : vendor.BankAccount + vendor.HoldGold;
// Mirrors AppendFees: a free vendor never runs out, and reports no periods at all.
sb.Append(charge > 0 ? (funds / charge).ToString() : "free").Append('|');
}
private static void AppendFees(StringBuilder sb, PlayerVendor vendor)
{
sb.Append(",\"fees\":{");