feat(bridge): protocol 5 — cutover 2a of 7 (edgemain) #20

Merged
whitlocktech merged 4 commits from edge into main 2026-09-01 13:55:04 +00:00
Member

Engagement Phase 13, step 2 of 7. Merge with link's cutover PR, in the same window — the emitter, overlay.toml and the sidecar's PROTOCOL_VERSION are one protocol bump with three declaration sites, and CI publishes the paired bundle from the two releases these produce.

Merge docs (step 1) first.

The three declaration sites, verified as agreeing

Site Value
servuo-plugins/overlay.toml protocol = 5 ← this PR
link/sidecar/src/main.rs PROTOCOL_VERSION 5 ← link's cutover PR
the module's uo_link_config.protocol default 5module-uo, step 4

The installer refuses to pair a sidecar and an overlay that disagree, so these two land together or a shard cannot be set up.

What lands (4 commits)

  • BridgeEvents.cshouse.decay gains ownerName and a decay schedule; a new account.login.result kind, which exists because EventSink.AccountLogin is a pre-decision veto hook and fires on successful logins too. A trigger on it would have mailed "someone tried to get into your account" every time the player logged in.
  • BridgeMarket.cs / BridgeSweeps.csvendor.listing gains ownerAcct and a fees block. Nothing anywhere on the wire carried a vendor's held gold or charge before this.
  • fix/vendor-fee-signature — the market sweep never diffed the new fees block, so a vendor running out of gold was invisible to it. Found by Phase 11b's live walk.
  • tools/scaffolding/ — the protocol-5 probe and rig driver. Never deployed.

Not in scope, deliberately

uo.vendor.sale stays in the opt-in patch tier. It is real and carries ownerAcct, but it needs a PlayerVendorSale EventSink the patches add to core, so moving it into overlay/ would mean shipping a core patch as though it were an overlay file. The consequence is documented rather than fixed: on a shard that declined the tier, the trigger is silently dormant.

All three enrichments were proved on the live rig — real ServUO 57.4 against the release sidecar.

  • AI-assisted: written with Claude Code.
**Engagement Phase 13, step 2 of 7.** Merge with `link`'s cutover PR, in the same window — the emitter, `overlay.toml` and the sidecar's `PROTOCOL_VERSION` are **one protocol bump with three declaration sites**, and CI publishes the paired bundle from the two releases these produce. **Merge `docs` (step 1) first.** ### The three declaration sites, verified as agreeing | Site | Value | | --- | --- | | `servuo-plugins/overlay.toml` | `protocol = 5` ← this PR | | `link/sidecar/src/main.rs` `PROTOCOL_VERSION` | `5` ← link's cutover PR | | the module's `uo_link_config.protocol` default | `5` ← `module-uo`, step 4 | The installer refuses to pair a sidecar and an overlay that disagree, so these two land together or a shard cannot be set up. ### What lands (4 commits) - **`BridgeEvents.cs`** — `house.decay` gains `ownerName` and a decay `schedule`; a new `account.login.result` kind, which exists because `EventSink.AccountLogin` is a *pre-decision veto hook* and fires on successful logins too. A trigger on it would have mailed "someone tried to get into your account" every time the player logged in. - **`BridgeMarket.cs` / `BridgeSweeps.cs`** — `vendor.listing` gains `ownerAcct` and a `fees` block. Nothing anywhere on the wire carried a vendor's held gold or charge before this. - **`fix/vendor-fee-signature`** — the market sweep never diffed the new `fees` block, so a vendor running out of gold was invisible to it. Found by Phase 11b's live walk. - **`tools/scaffolding/`** — the protocol-5 probe and rig driver. Never deployed. ### Not in scope, deliberately `uo.vendor.sale` stays in the **opt-in patch tier**. It is real and carries `ownerAcct`, but it needs a `PlayerVendorSale` EventSink the patches *add* to core, so moving it into `overlay/` would mean shipping a core patch as though it were an overlay file. The consequence is documented rather than fixed: on a shard that declined the tier, the trigger is silently dormant. All three enrichments were proved on the live rig — real ServUO 57.4 against the release sidecar. - [x] AI-assisted: written with Claude Code.
wtclaude added 4 commits 2026-09-01 13:45:24 +00:00
Three emitter changes and the overlay's protocol declaration, in one PR because
"The bridge is a contract": overlay.toml must be bumped in the same change as the
emitters or the next bundle silently fails to compose.

BridgeSweeps — house.decay gains ownerName and a nested `schedule`
{dynamicDecay, nextStage, decayPeriodSec, estimatedCollapse}.

estimatedCollapse is emitted only where ServUO can actually know it. Dynamic decay
(Core.ML) draws each stage's duration at RANDOM when the stage is entered, so
NextDecayStage is exact for the next transition and nothing beyond it is known —
collapse becomes exact only at IDOC, where the next transition IS the collapse.
Static decay is a pure function of LastRefreshed and DecayPeriod, so it is exact at
every stage. Emitting it anywhere else would publish a guess as a fact, and on the
website's side that becomes a dated promise in someone's mail.

BridgeMarket — vendor.listing gains ownerAcct and a nested `fees` block.

ownerAcct is the one that matters structurally: the frame has carried ownerName
since v3, but a character name joins to nothing — only the game account is the
website's link key. The fees block resolves PlayerVendor.PayTimer's dismissal rule
(pay > totalGold => Destroy) on the shard, because both halves of that comparison
differ between ServUO's two vendor systems and re-deriving them downstream would be
a second implementation of a rule that lives in core.

No daysRemaining: under the old vendor system a pay period is a UO day
(Clock.MinutesPerUODay, about two real hours), so the obvious name would be wrong
by a factor of twelve on exactly the shards least likely to notice. periodsRemaining
plus the interval, and dismissalAt as an instant. A commission vendor has no pay
timer at all and reports exempt with no schedule — "never dismissed" is not the same
as "dismissed in 400 days".

BridgeEvents — a new account.login.result kind.

EventSink.AccountLogin is a veto hook that fires BEFORE the auth decision, and
AccountLoginEventArgs constructs with Accepted = true, so the existing
account.login.attempt fires on successful logins too and cannot carry a verdict. A
security rule built on it would have mailed "someone tried to get into your account"
every time the player logged in.

The verdict is read one Core slice later via DelayCall(Zero). That needs no core
patch AND does not depend on handler subscription order, which ServUO does not define
and a shard's own scripts can change. reason is omitted on an accept, because
ALRReason's zero value is Invalid and would read as a failure reason. The address is
resolved inside the handler, since AccountLogin_ReplyRej disposes the NetState before
the deferred read runs. The password is never read, logged or emitted.

tools/scaffolding/BridgeProtocol5Probe.cs drives all three on a live shard, and the
README records the two traps it took to get there — both of which produce SILENCE
rather than an error, so each looks exactly like a broken emitter:

  * An in-process login probe can never produce accepted:true. AccountHandler calls
    acct.HasAccess(e.State) BEFORE it checks the password, and a null NetState fails
    that. Only a real socket proves the accepted half — and it is the better test
    anyway, since it also produces the real ip.
  * Forcing a decay stage on a house that cannot decay emits nothing at all. Only
    Condemned and ManualRefresh houses decay; an AutoRefresh one — and the owner's
    NEWEST house is always AutoRefresh — has a DecayLevel getter that calls
    ResetDynamicDecay() and reports Ageless, wiping the forced stage before the sweep
    reads it.

Verified on the local rig against the release sidecar: a house walked
Fairly -> Greatly -> IDOC carried estimatedCollapse on the IDOC frame and only there;
every vendor's periodsRemaining matched funds/chargePerPeriod, including one at 0
whose dismissalAt equals its next tick; a real socket login gave
accepted:false reason:BadPass and then accepted:true. Compiles clean against ServUO
57.4 reference assemblies.

Docs: RunicGateway/docs link/v5.md.

Co-Authored-By: Claude <noreply@anthropic.com>
Reviewed-on: #18
`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>
Reviewed-on: #19
whitlocktech approved these changes 2026-09-01 13:54:58 +00:00
whitlocktech merged commit 827de04471 into main 2026-09-01 13:55:04 +00:00
whitlocktech deleted branch edge 2026-09-01 13:55:04 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RunicGateway/servuo-plugins#20
No description provided.