feat(shard): ingest protocol 5 — decay schedule, vendor fees, login result #21
Reference in New Issue
Block a user
No description provided.
Delete Branch "feature/protocol-v5"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The website half of the protocol-5 bump. Engagement Phase 10. Design of record: docs#193 →
link/v5.md. Companion PRs: servuo-plugins#18, link#34, docs#193.Schema — twelve columns, two indexes
shard_housesgainsnext_stage,estimated_collapse,decay_period_sec,dynamic_decay.estimated_collapsestays null far more often than not, deliberately: under dynamic decay ServUO draws each stage at random on entry, so collapse is knowable only at IDOC. A null means "not knowable", never "not yet read".shard_vendorsgainsowner_acctplus seven fee columns and an index ondismissal_at.owner_acctis the structural one — the table has carriedowner_namesince protocol 3, but a character name joins to nothing, and only the game account reachesshard_account_links. Until now a vendor row named an owner the site could not resolve to a person.dismissal_at+owner_acctare what let Phase 11'suo.vendor.expiringfind "vendors about to be dismissed" and turn each into a person, without scanning every shop.Ingest
Both new field groups arrive nested, are flattened into columns on the way in, and re-nested on the way out — the same trick
shardMarketalready uses forlocation. Not stylistic: the visibility projection matches literal JSON keys, so the stored read model and the live wire frame must spell a group identically or one admin rule covers only one of the two paths. It also means a field added inside a group later inherits the group's gate instead of defaulting to visible; there is a test that adds an imaginary future fee field and asserts exactly that.Two write-back asymmetries, both load-bearing:
ownerNameis written only when the frame carries one.house.updatealso writes that column, from a different sweep, and a pre-v5 overlay'shouse.decaycarries noownerNameat all — coalescing to null would let every decay transition erase a name the registry had already resolved.dismissalAtis taken from the shard rather than recomputed — the shard resolved it against ServUO's two vendor systems, whose charge, funds and pay interval all differ, and re-deriving it here would be a second implementation ofPlayerVendor's own rule.Visibility — three classifications, each chosen rather than inherited
house.decay→scheduleanonymousvendor.listing→feesadminmarketfeature that does not reproduce prior behaviour, because there is no prior behaviour to reproduce. Shop name, owner and location are already visible to any player through the in-game Vendor Search gump — that is the argument for publishing them. Held gold, daily charge and dismissal date are visible to the OWNER only, on that vendor's own gump. Publishing them anonymously would be a new disclosure and a targeting aid: which shops are about to be abandoned, and how much coin is in eachaccount.login.resultKIND_FEATUREis the map of kinds an admin may widen, and there is no rung below admin that an IP plus an auth verdict belongs on. The omission is the decision, and a test says so by nameowner_acctneeds no rule — rule 1 locks it by suffix. And the new columns are in no REST read model's column list: they exist for Phase 11's server-side trigger and reach no client at all.The pin, and the protocol-4 bug seen from the other side
Both declaration sites go to 5 (the model constant and
schema.sql'sCREATEdefault), plus the one-shot migration guardedprotocol < 5so an install that missed an earlier step is carried the whole way.The schema test used to assert
DEFAULT 4at each site. That is exactly how protocol 4 shipped with the emitters moved and one site left behind: every site agreed with itself and the test passed. It now readsDEFAULT_PROTOCOLfrom the model, so the assertion is "the declarations agree" — and the one-shot migration test is written once against the current version instead of being hand-copied per bump.Verification
470 tests pass, 16 new. Verified end to end on the live rig against a real ServUO 57.4 and the release sidecar: the decay schedule with
estimatedCollapseonly at IDOC, every vendor'speriodsRemainingmatchingfunds / chargePerPeriod, and both outcomes ofaccount.login.result.🤖 Generated with Claude Code
The website half of the protocol-5 bump. Engagement Phase 10. Schema — twelve columns and two indexes. shard_houses gains next_stage, estimated_collapse, decay_period_sec and dynamic_decay. estimated_collapse is nullable and stays null far more often than not, deliberately: under dynamic decay ServUO draws each stage at random on entry, so collapse is knowable only at IDOC. A null means "not knowable", never "not yet read". shard_vendors gains owner_acct plus seven fee columns and an index on dismissal_at. owner_acct is the structural one — the table has carried owner_name since protocol 3, but a character name joins to nothing, and only the game account reaches shard_account_links. Until now a vendor row named an owner the site could not resolve to a person. dismissal_at + owner_acct are what let Phase 11's uo.vendor.expiring find "vendors about to be dismissed" and turn each into a person, without scanning every shop. Ingest. Both new field groups arrive NESTED and are flattened into columns on the way in, then re-nested on the way out — the same trick shardMarket already uses for `location`. That is not stylistic: the visibility projection matches literal JSON keys, so the stored read model and the live wire frame have to spell a group identically or one admin rule covers only one of the two paths. It also means a field added inside a group later inherits the group's gate instead of defaulting to visible; there is a test that adds an imaginary future fee field and asserts exactly that. Two write-back asymmetries, both load-bearing: * ownerName is written ONLY when the frame carries one. house.update also writes that column, from a different sweep, and a pre-v5 overlay's house.decay carries no ownerName at all — coalescing to null would let every decay transition erase a name the registry had already resolved. * The schedule and fee columns are written UNCONDITIONALLY, including as nulls. A schedule is a claim about the future and goes stale on its own: roll a shard back to a pre-v5 overlay, or let a house leave IDOC, and the right stored value is nothing. A dismissal date nobody is maintaining is worse than none. dismissalAt is taken from the shard rather than recomputed. The shard resolved it against ServUO's two vendor systems, whose charge, funds and pay interval all differ; re-deriving it here would be a second implementation of PlayerVendor's own rule. Visibility — three classifications, each chosen rather than inherited. * house.decay's `schedule` defaults to `anonymous`. The countdown IS the public IDOC page's content and a house at IDOC is already announced in game. Listed anyway so a shard that considers a precise collapse time an unfair advantage can raise it — and one nested rule takes the whole schedule with it. * vendor.listing's `fees` defaults to `admin`, the only default in the market feature that does not reproduce prior behaviour, because there is no prior behaviour to reproduce. Shop name, owner and location are already visible to any player through the in-game Vendor Search gump, which is the argument for publishing them. Held gold, daily charge and dismissal date are visible to the OWNER only, on that vendor's own gump. Publishing them anonymously would be a new disclosure and a targeting aid — which shops are about to be abandoned, and how much coin is in each. * account.login.result is admin-only BY OMISSION. KIND_FEATURE is the map of kinds an admin may widen, and there is no rung below admin that an IP plus an auth verdict belongs on. The omission is the decision, and a test says so by name. owner_acct needs no rule: rule 1 locks it by suffix. And the new columns are in no REST read model's column list — they exist for Phase 11's server-side trigger and reach no client at all. The pin, and the protocol-4 bug seen from the other side. Both declaration sites go to 5 (the model constant and schema.sql's CREATE default), plus the one-shot migration, guarded `protocol < 5` so an install that missed an earlier step is carried the whole way. The schema test used to assert `DEFAULT 4` at each site. That is exactly how protocol 4 shipped with the emitters moved and one site left behind: every site agreed with itself and the test passed. It now reads DEFAULT_PROTOCOL from the model, so the assertion is "the declarations AGREE", and the one-shot migration test is written once against the current version instead of being hand-copied per bump. 470 tests pass, 16 new. Verified end to end on the live rig against a real ServUO and the release sidecar. Docs: RunicGateway/docs link/v5.md. Co-Authored-By: Claude <noreply@anthropic.com>