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:
@@ -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\":{");
|
||||
|
||||
Reference in New Issue
Block a user