docs(link): protocol 5 — the decay schedule, vendor fee state and account.login.result

Design of record for the bump, plus the reference updates it obliges.
Engagement Phase 10, as built.

link/v5.md is new. Three enrichments in one bump, because a bump costs a sidecar
release, a republished bundle and an operator update on every shard — a field left
out costs a whole second round of that, not a follow-up commit.

  a. house.decay gains ownerName and a nested `schedule`. estimatedCollapse is
     present ONLY where it is exact: under dynamic decay each stage's duration is
     drawn at random on entry, so collapse is knowable only once the house is
     already at IDOC; under static decay it is exact at every stage. Omitted
     rather than approximated — an absent field is honest where a wrong date
     becomes a dated promise in a player's mail.
  b. vendor.listing gains ownerAcct — a character name identifies nobody, only the
     game account joins to shard_account_links — and a `fees` block resolving
     PlayerVendor.PayTimer's dismissal rule into a dismissalAt instant. There is
     deliberately no daysRemaining: a pay period is a UO day on an old-system
     shard, so the name would be wrong by a factor of twelve.
  c. account.login.result is a NEW kind. account.login.attempt fires from a veto
     hook that runs before the auth decision and constructs with Accepted = true,
     so it fires on successful logins too and cannot carry a verdict.

INTEGRATION.md: the version banner to 5, the Housing and Market field tables and
examples, and the new kind beside the attempt — with the attempt's own row
corrected to say why it is not a failure signal.

PLAN.md: 5.1's "failed-login signals" row was wrong about what AccountLogin can
tell you; 5.3 and 5.4 gain the two new signals; 5.4's dynamic-decay model gets the
consequence for publishing a collapse time, and the CanDecay rule gets its testing
trap. 7's frames are marked as 1.0's design sketch rather than the shipped wire,
which they have not been for four versions.

ENGAGEMENT.md: Phase 10 as built. Two decisions taken during the build (both field
groups nested, so one admin rule governs each and a future field inherits the gate;
no daysRemaining), the three visibility classifications, and the pin improvement —
the schema test now asserts the declarations AGREE rather than that they all say a
literal, which is the protocol-4 bug seen from the other side.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-31 19:19:09 -05:00
parent efce1d88aa
commit 6304c42dfb
5 changed files with 432 additions and 16 deletions

View File

@@ -103,7 +103,7 @@ Read config in `Configure`. Subscribe events in `Initialize`. Open the socket an
| Player online | `EventSink.Login` | low | Best per-player anchor. Snapshot account, char, serial, map, loc. |
| Player offline | `EventSink.Logout` | low | Pair with Login. |
| Socket up/down | `Connected` / `Disconnected` | low | Lower level; fires at char-select too. |
| Auth attempts | `AccountLogin`, `GameLogin` | low | Failed-login / IP signals for the website. |
| Auth attempts | `AccountLogin`, `GameLogin` | low | IP signals for the website. **`AccountLogin` cannot give you a FAILED login**, which this row assumed it could: it is a veto hook that fires *before* the auth decision, and `AccountLoginEventArgs` constructs with `Accepted = true`, so a handler reading the verdict there reports every login as accepted. Protocol 5 adds `account.login.result`, read one Core slice later — [`v5.md`](v5.md) §2.3. |
| Roster change | `CharacterCreated`, `DeleteRequest` | rare | Keep the sidecar's roster cache honest. |
| Client fingerprint | `ClientVersionReceived`, `ClientTypeReceived` | low | Classic vs Enhanced; version enforcement. |
@@ -133,6 +133,7 @@ Read config in `Configure`. Subscribe events in `Initialize`. Open the socket an
| **Player vendor sale** | ⚑ **needs core edit** | medium | See §6. The one non-drop-in piece. |
| Vendor placed | `PlacePlayerVendor` | rare | `PlayerVendorDeed.cs:60,106`, `VendorRentalGumps.cs:418`. Tracks vendor population. |
| Vendor listings | vendor snapshot sweep / on demand | periodic | **0.0003 ms/listing.** Serial, itemId, price, `IsForSale`, `HoldGold`. |
| **Vendor fee state** | vendor sweep (Protocol 5) | periodic | The dismissal deadline, not just the balance. `PlayerVendor.PayTimer` destroys a vendor when `pay > totalGold`, and **both halves differ between ServUO's two vendor systems**`ChargePerRealWorldDay` vs `ChargePerDay`, `HoldGold` vs `BankAccount + HoldGold`, a real day vs a UO day. Resolved on the shard into a `dismissalAt` instant; see [`v5.md`](v5.md) §2.2. `IsCommission` vendors have no pay timer at all. |
| Item consumed | `OnConsume` | medium | Regs, potions — consumption side of the economy. |
> ⚠️ `ValidVendorPurchase` / `ValidVendorSell` are **validation-stage veto hooks**, not "sale committed" callbacks. Treat as *sale attempted*; reconcile against `AccountGoldChange` if you need ledger accuracy. **Never block or throw in them.**
@@ -144,6 +145,7 @@ Note: `CurrencyThreshold` is **1,000,000,000** on this shard. `TotalCurrency` is
| Signal | Hook | Freq | Notes |
|--------|------|:----:|-------|
| Decay transition | decay sweep, emit on change | 3060 s | **0.0002 ms/house.** No EventSink exists. |
| **Decay schedule** | same sweep (Protocol 5) | with the above | `NextDecayStage`, `DecayPeriod`, and a collapse time **only where one exists** — see the note below. |
Hold a `Dictionary<Serial, DecayLevel>` and emit only on transition. On `ServerStarted`, take a **silent baseline pass** (populate without emitting), or every house re-announces its stage on every boot. Optionally emit one `idoc.snapshot` for houses already at IDOC/Collapsed, clearly flagged as a snapshot.
@@ -153,6 +155,20 @@ Hold a `Dictionary<Serial, DecayLevel>` and emit only on transition. On `ServerS
- **`BaseHouse.CanDecay` is true only for `DecayType.Condemned` or `DecayType.ManualRefresh`** (`BaseHouse.cs:136-157`). An active owner's *newest* house is `AutoRefresh` and **never decays**. So a house reaches IDOC only when the owner account is inactive (`LastLogin` older than `Account.InactiveDuration`, 180 days → `Condemned`) or the house is not the owner's newest.
- Any account with `AccessLevel >= GameMaster` — or **any character on it** — makes all its houses `Ageless`.
**What that model means for publishing a collapse time (Protocol 5).** Because each stage's
duration is drawn at random *when the stage is entered*, `NextDecayStage` is exact for the next
transition and **nothing beyond it is known at all**. A collapse time is therefore exact only once
the house is already at IDOC, where the next transition is the collapse. `house.decay` omits
`estimatedCollapse` everywhere else rather than approximating it — the field's absence means "not
knowable", never "not yet read". On a shard where `DynamicDecay.Enabled` is false the dead code
above is live instead, and collapse is exact at every stage; the frame carries `dynamicDecay` so a
consumer can tell which regime it is reading. [`v5.md`](v5.md) §2.1.
**The `CanDecay` rule above is also the main trap when TESTING this.** Forcing a stage with
`SetDynamicDecay` on an `AutoRefresh` house does nothing observable: the `DecayLevel` getter calls
`ResetDynamicDecay()` and reports `Ageless`, so the forced stage is wiped before the next sweep
reads it and **no frame is emitted at all** — which looks exactly like a broken emitter.
Payload per transition: house serial, `from``to` level, `X/Y/Z`, `Map`, `BanLocation`, `Region.Name`, `Sign?.GetName()`, owner serial + account, co-owners, `BuiltOn`, `LastRefreshed`, `NextDecayStage`. Guard `Owner`/`Sign`/`Region` for null (abandoned or mid-demolition). Read `house.DecayLevel` **once per house per sweep** into a local — the getter is computed and mutates `m_CurrentStage`.
### 5.5 Combat, death, PvP
@@ -233,6 +249,13 @@ Three edits, then the bridge stays pure-subscription:
Newline-delimited JSON, one object per line, `serial` as the primary key.
> **The frames below are 1.0's design sketch, not the shipped wire.** They have drifted in the four
> versions since — `house.decay` in particular ships a flattened `ownerSerial`/`ownerAcct`/`ownerName`
> rather than an `owner` object, and from Protocol 5 its `nextStage` lives inside a nested `schedule`
> alongside `decayPeriodSec`, `dynamicDecay` and a conditional `estimatedCollapse`. The normative
> field lists are [`INTEGRATION.md`](INTEGRATION.md) §Event catalog, with each bump's rationale in
> [`PROTOCOL_2.md`](PROTOCOL_2.md), [`v3.md`](v3.md), [`v4.md`](v4.md) and [`v5.md`](v5.md).
### Outbound (shard → sidecar)
```jsonc