Skip to content

refactor(gateway): split WorldStore into one Store per Protocol Family - #644

Merged
Mathih13 merged 20 commits into
mainfrom
refactor/gateway-store-families
Oct 10, 2026
Merged

Mathih13 merged 20 commits into
mainfrom
refactor/gateway-store-families

Conversation

@Mathih13

Copy link
Copy Markdown
Contributor

A Gateway adapter had to satisfy about 290 methods, because WorldStore carried 185 methods of its own and 15 family traits as supertraits. 85 methods had default bodies, and 78 of those never ran. Every family change edited one 4,800-line shared Fake in world/tests.rs, and every test that entered the world counted a fixed 13 packets.

This is phase 3 of #635. It changes no behaviour.

Store

  • WorldStore has no methods now. It is an umbrella over one Store trait per Protocol Family (session, shard routing, character, transfer, party, mail, social, npc, trainer, bank, combat, death, trade, loot roll, speech, and the existing action families), with a blanket impl.
  • No Store trait has a default body. A method an adapter does not implement fails to compile.
  • The Coordinator implements each family in gateway/src/stdb/store/<family>.rs. The bodies are moved verbatim; phase 4 collapses them.
  • Handlers and helpers bound on the families they call, chosen by the compiler.
  • The World Session holds one RoutedStore instead of home plus on_home_shard!. A None routing answer keeps the current pin, and leaving the world clears it, as before.
  • Deleted dead production code that only the removed defaults reached: home_shard, publish_shard_index, realm_core::settle_shard_index, realm_core::publish_shard_index, config::resolve_home_shard and HomeShard, with the 11 tests that only covered them. locate_home_shard keeps its own tests of the same rules.

Tests

  • The shared Fake moved to gateway/src/world/test_support/ as WorldFake, with one state struct per family in world_fake/<family>.rs. It keeps the multi-shard topology that party, transfer, mail, presence, who, whisper, loot rolls and shard routing need.
  • drain_world_entry reads up to the SMSG_WEATHER that closes the world-entry batch. The fixed packet counts are gone.
  • world/tests.rs is split into one test file per family plus session_tests.rs.
  • Trainer, bank, combat, trade, death, loot window, vendor, auction, item, taxi, melee, cast, guild, chat, channel and weather tests now run their handler against a family-sized Fake. Tests that asserted a Fake's call log now assert the outcome. Session-level tests (handshake, world entry, routing, logout order, flood limit) stay on the socket.
  • Quest, npc, speech and character select still run on WorldFake, because their handlers are bound on many families. Phase 5 narrows them.
  • Gateway unit tests: 1904 before the conversion, 1891 after. Each removed test duplicated a dispatcher test, and the conversion agents named the covering test for each.

Glossary: adds Protocol Family and Speech.

Local gates: fmt, clippy with -D warnings, the five crate test commands and the wasm check pass.

🤖 Generated with Claude Code (Claude Opus 5.5 lead, with Opus 5.5 and Sonnet 5.5 subagents)

Mathih13 and others added 19 commits October 10, 2026 16:45
WorldStore had 185 own methods, 85 of them with default bodies. A Fake
silently answered with a default for any method it did not write.

Each method now sits on the family trait whose handler calls it:
ShardRouting, Session, Character, Transfer, Party, Mail, Social, Npc,
Trainer, Bank, Combat, Death and Trade are new, and LootWindowStore and
ChatActionStore grow. WorldStore is a method-less umbrella with a
blanket impl. Every default body is gone. The Coordinator adapter moves
to one file per family under stdb/store, and InMemoryStore implements
each family in its own block, with the seven answers it took from
defaults now written out.

Two methods had no caller once the defaults went: home_shard and
publish_shard_index. They leave the traits, the Fake keeps them as
inherent helpers, and the production code only they reached is deleted:
realm_core::settle_shard_index, realm_core::publish_shard_index,
config::resolve_home_shard, HomeShard, and the tests of those functions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The World Session carried a home pin, a generic store and the
on_home_shard! macro that picked between them at each call. Handlers
were bound on all of WorldStore, so a signature did not show which
families a handler needed.

RoutedStore now holds the listener's handle and the pinned Home Shard.
route_home pins only when settle_home_shard names a new shard, and
leave_world clears the pin. run_world_session takes an
Arc<dyn WorldStore>, and each frame dispatches against
RoutedStore::current(). Every old-style handler and helper is bound on
exactly the Store families it calls. Helpers that use their own store
as the Realm-core handle on a single-database gateway keep the
WorldStore bound.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LootWindowStore carried the eight Loot Roll methods and ChatActionStore
the three Speech methods, so the loot window, chat and channel Fakes held
unreachable stubs for calls their dispatchers never make.

LootRollStore (world/loot.rs) now holds the vote, the master looter's
assignment and the promotion relay's reads and requests. SpeechStore
(handlers/chat.rs) holds say, yell and `/e` lines, text emotes and the GM
dot-command. Each caller is bounded on the families it calls, and the
Coordinator adapters move to stdb/store. Speech joins both glossaries.

The Module comments that named the deleted home_shard resolver now name
locate_home_shard, which settle_home_shard runs at every world entry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…by Store family

The shared Fake held 223 flat fields. Each field now sits in the state
struct of the Store family whose methods read it, so a test reads
`store.<family>.x`. Three fixtures several families read stay on
WorldFake itself: characters, npc_refuses and trade_error.

The field moves and the struct literals were rewritten mechanically and
checked by the compiler. No test changes what it asserts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
WorldFake and its fixtures leave the 13,000-line world/tests.rs. Each
Store family's state struct and impl now sit together in
test_support/world_fake/<family>.rs, and shard routing sits in
test_support/topology.rs. A later change to one family's Fake edits one
file.

Code motion only: items gain pub(crate) so the test modules reach them,
and paths that named a parent module through `super::` now name it from
the crate root.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Forty-seven socket tests read a fixed 13 frames (12 on a world-port)
to get past world entry, so a change to the login sequence edited every
one of them. drain_world_entry reads up to and including the
SMSG_WEATHER that always closes the entry batch, and fails after 64
frames. Tests that inspect the batch iterate the drained frames, so
their assertions are unchanged. WORLD_ENTRY_PACKETS and
WORLD_REENTRY_PACKETS are gone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
world/tests.rs held 194 socket tests across every family. They now live
in one file per family (auction, bank, cast, channel, character, chat,
combat, death, guild, item, loot_window, melee, npc, quest, social,
taxi, trainer, vendor, weather) or join the existing party, loot, who
and whisper files. session_tests.rs takes the handshake, admission,
world entry, movement, coalescing and logout-order tests.

The world-entry helpers (handshake, tester_store, quest_store,
enter_world) move to test_support/entry.rs. tests.rs keeps the shared
imports, the module list and the helpers several files use. Each test
keeps its name and body.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Death requests run through handle_loot against HandleLootFake, which models who is
alive and asserts outcomes. Loot window socket tests become dispatcher tests on
InMemoryLootWindow. The socket tests that remain check GameObject type routing.
WorldFake keeps only the loot window state that session and routing tests read.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Each family drives its handler through a local run helper against a Fake that
implements only the Stores the handler is bounded on. Tests assert the packets
sent or the Fake's resulting state, not a call log. The four families lose their
state from WorldFake, except the fields other tests still read.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…t the socket

Guild, chat and channel opcode behaviour was already covered by the
InMemory*Actions dispatcher tests. Delete the socket duplicates, add the one
guild membership opcode test the dispatcher lacked, and test weather through
zone_weather_message with a local Fake. Session-level guild, chat and cleanup
tests stay on WorldFake. Remove the channel state and the Fake fields no
test reads.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… unused

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…Fakes with the family tests

The handler test modules become reachable from the World Session tests. The vendor, taxi, melee
and cast Fakes model state a test can read back (buyback ring, accepted route, engagements, auras,
pending casts), so a test can assert an outcome and not a call log.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…sts at the handler

Each family's behaviour tests call its dispatcher through a local run helper against the family
Fake. Session-level tests stay on WorldFake: the buyback replay at world entry, the raw auction
browse, the auction refusal followed by WHO, and the two taxi gossip tests. WorldFake loses the
item and melee state and most of the vendor, taxi and cast state.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…way-store-families

# Conflicts:
#	gateway/src/world/test_support/world_fake/mod.rs
…way-store-families

# Conflicts:
#	gateway/src/world/tests.rs
…ness

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copy link
Copy Markdown
Contributor Author

Review of efff177: Standards and Spec

This review has two axes. The Standards axis checks the code against CODING_STANDARDS.md, AGENTS.md, CONTEXT.md and a baseline of code smells. The Spec axis checks it against Phase 3 of #635. The two axes are not ranked against each other.

Priority: P1 fix before merge · P2 should fix, or state it as a known gap · P3 optional.

Standards

Correctness. I found no production regression. RoutedStore keeps the old on_home_shard! behaviour: a None from settle_home_shard keeps the pin, leave_world unpins after the claim release, and dispatch reads the handle once per frame. The deleted home_shard, settle_shard_index and resolve_home_shard were dead code. No Store trait still has a default body.

Documented standards (hard)

  • P2: the Protocol Family definition contradicts the code. CONTEXT.md:1232 says "a handler takes only its family's Store". But handle_char is bound on four Stores (handlers/char.rs:328), handle_loot on five, and enter_world and dispatch take &dyn WorldStore. CODING_STANDARDS.md treats stale explanation as a defect. Change the definition to describe the current state, or narrow the bounds.
  • P2: glossary drift on "family". The new entry lists _Avoid_: family (unqualified), but new prose says "Store family", "family Fake" and "every family": world/store.rs:1-3,14-15, stdb/store/mod.rs:1, test_support/family.rs, world_fake/mod.rs:1,59-62 and docs/recipes/add-an-opcode.md step 8. The definition itself says "the vendor family". Use "Protocol Family" consistently, or relax the Avoid line.
  • P3: an issue number in a comment you rewrote. world_fake/mod.rs:65 reads /// 195: npc_refuses_interaction …. CODING_STANDARDS.md says "Do not put issue numbers in code comments", and the boy scout rule applies.

Baseline smells (judgement calls)

  • P2: Duplicated Code and Shotgun Surgery. world/store.rs:16-48 and :52-85 list the same 30 bounds twice, once for the trait and once for the blanket impl. The add-an-opcode recipe now has a contributor edit both lists.
  • P2: Refused Bequest. world/handle_loot_fake.rs has 27 unreachable! stubs. The npc_store_refusing_by! macro (family.rs:61-140) has 13 unimplemented! stubs. Both show that NpcStore and the handle_loot bounds are wider than their callers need.
  • P3: Fakes live in three places. They are in test_support/world_fake, in world/handle_loot_fake.rs (loaded through #[path] from death_tests.rs:6), and in handlers/*::tests, re-exported under #[cfg(test)] (handlers/mod.rs).
  • P3: Mysterious Name. WorldFake.trade_error (world_fake/mod.rs:69) is read only by the npc, party, quest and social Fakes, never by trade. RoutedStore.default (world/mod.rs:434) holds the listener's handle.
  • P3: two ways to reach the Store. handle_char receives a family Store, but the login and world-port paths read conn.store.current() (char.rs:424-425, 487-489).
  • P3: stale panic text. In world_fake/mod.rs:96-98, the lk panic says "see the fn doc on lk", but lk has no doc comment.

Spec (#635 Phase 3)

(a) Missing or partial

  • P1: the PR description overstates what this PR delivers. Phase 3 says "The shared InMemoryStore shrinks to the multi-shard topology the transfer and party families need" and "Each family trait has its own small Fake next to its tests". WorldFake (world_fake/mod.rs:57-92) still carries state for about 21 families across 29 impl files. Mail, party, transfer, session, guild, member stats, presence, social, character, npc, death, vendor and auction tests still use it. Mail is one of the families the spec names. Either do the shrink, or list it as an open item for phase 5 in the PR and in Contributor readiness: retire text-pinning tests, cut onboarding, split the Store seam #635. Right now only quest, npc, speech and character select are named as deferred.
  • P2: the phase proof does not hold yet. The spec says "Phase 3 proves: each family's tests compile against only that family's Fake." That is not true while most families still use the shared WorldFake.
  • P2: WorldStore still lists the family traits as supertraits. The spec says "Remove the fifteen family traits from the WorldStore supertrait list." store.rs:16-46 keeps 30 supertraits as a method-less umbrella. Handlers do bind on families, which meets the intent, but ShardRoutingStore returns Arc<dyn WorldStore> (store.rs:128-148). That brings the umbrella back into party (party.rs at about 20 sites), mail.rs:777, loot.rs:131,183, party_mirror.rs:94-95, enter_world and dispatch.
  • P2: tests still assert on call-log order. The spec says a good test "never pins a call sequence, and never asserts on a Fake's recording format". These still do: guild_tests.rs:30 (position(&calls, "guild_op:SignOff")), tests.rs:204 position(), and TopologyState.calls as read in party_tests.rs and transfer_tests.rs.

(b) Not asked for

  • P3: the dead-code deletion in gateway/src/config.rs and gateway/src/realm_core.rs (11 tests) goes past the spec, but I confirmed the code was dead. The module/ changes touch comments only, so there is no schema or reducer change.
  • P2: about 23 socket tests were deleted as duplicates in 0d44b6e and 83a56e6, for example messagechat_* and a_weather_read_failure_still_completes_world_entry. The spec asks for a split by family, not a smaller suite. Most of them have handler-level equivalents. Please check that a_weather_read_failure_still_completes_world_entry has one, because it covers session behaviour. Also, #[test] in gateway/src counts 1922 → 1898 (−24), but the PR says 1904 → 1891 (−13). Please reconcile the two numbers.

(c) Implemented, but check

  • P3: drain_world_entry (test_support/entry.rs:15-30) stops at SMSG_WEATHER. Weather closes the batch today, so this works. But it is not a dedicated end marker: a frame that arrives after weather goes unread. A comment that names this assumption would help the next person who changes login order.

Summary. Standards: 9 findings. The worst is the Protocol Family definition, which says handlers take only their own family's Store when several do not. Spec: 8 findings. The worst is that WorldFake was not narrowed to the multi-shard families, and that gap is not stated as deferred.


Generated by Claude Code

…s over a World Session

The World Fake now records use_gameobject and take_loot calls.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@Mathih13
Mathih13 merged commit a612198 into main Oct 10, 2026
9 checks passed
@Mathih13
Mathih13 deleted the refactor/gateway-store-families branch October 11, 2026 18:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant