Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8a0134f
refactor(gateway): split WorldStore into Store family traits
Mathih13 Oct 10, 2026
6bbc387
refactor(gateway): route the World Session through RoutedStore
Mathih13 Oct 10, 2026
cea792f
docs(gateway): carry the comment sweep into the moved Store declarations
Mathih13 Oct 10, 2026
23f8e84
refactor(gateway): give Loot Rolls and Speech their own Store families
Mathih13 Oct 10, 2026
7749cf2
test(gateway): rename InMemoryStore to WorldFake and group its state …
Mathih13 Oct 10, 2026
4cdf571
test(gateway): move WorldFake into world/test_support
Mathih13 Oct 10, 2026
f802650
test(gateway): drain world entry up to its SMSG_WEATHER
Mathih13 Oct 10, 2026
97165d4
test(gateway): split the World Session socket tests by Store family
Mathih13 Oct 10, 2026
83a56e6
test(gateway): test loot window and death requests at the handler
Mathih13 Oct 10, 2026
5dd1f82
test(gateway): run trainer, bank, combat and trade tests at the handler
Mathih13 Oct 10, 2026
0d44b6e
test(gateway): test guild, chat, channel and weather behaviour withou…
Mathih13 Oct 10, 2026
99a26df
test(gateway): drop imports the loot window and death conversion left…
Mathih13 Oct 10, 2026
4b15c77
test(gateway): share the vendor, auction, item, taxi, melee and cast …
Mathih13 Oct 10, 2026
c735ef4
test(gateway): run the vendor, auction, item, taxi, melee and cast te…
Mathih13 Oct 10, 2026
7aa4833
Merge remote-tracking branch 'origin/p3/convert-b' into refactor/gate…
Mathih13 Oct 10, 2026
ea4b963
Merge remote-tracking branch 'origin/p3/convert-c' into refactor/gate…
Mathih13 Oct 10, 2026
4a5d046
Merge remote-tracking branch 'origin/p3/convert-d' into refactor/gate…
Mathih13 Oct 10, 2026
4e5d641
docs(gateway): name the Protocol Family and share the family test har…
Mathih13 Oct 10, 2026
efff177
style(gateway): sort the test re-exports
Mathih13 Oct 10, 2026
22c97b0
test(gateway): cover GameObject use routing for chests and other type…
Mathih13 Oct 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ A World of Warcraft 1.12.1 server. Game state lives in SpacetimeDB; the gateway

**O** [Officer Note](#guilds) · [Official Package Collection](#packages) · [Official Package Source](#packages) · [Open Role](#meeting-stones) · [Operator](#realm-topology) · [Owner Token](#gateway-and-module)

**P** [Package](#packages) · [Package API](#packages) · [Package Cast Range](#packages) · [Package Config](#packages) · [Package Creature Range](#packages) · [Package Delta](#packages) · [Package Event](#runtime-scripts) · [Package EventAI Range](#packages) · [Package Fixture](#packages) · [Package Gameobject Range](#packages) · [Package Globals Range](#packages) · [Package Gossip Range](#packages) · [Package Identifier Range](#packages) · [Package Import](#packages) · [Package Inventory](#packages) · [Package Item Range](#packages) · [Package Loot Range](#packages) · [Package Quest Range](#packages) · [Package Script Range](#packages) · [Package Source](#packages) · [Package Spell](#packages) · [Package Spell Metadata Range](#packages) · [Package Spell Range](#packages) · [Package Teardown](#packages) · [Package Trainer Range](#packages) · [Package-owned Account](#accounts-characters-and-sessions) · [Party](#sharding-and-transfer) · [Party Partition](#sharding-and-transfer) · [Patrol Pause](#creature-ai) · [Petition](#guilds) · [Petitioner](#guilds) · [Pre-auth I/O Deadline](#accounts-characters-and-sessions) · [Proc](#procs) · [Proficiency](#gateway-and-module) · [Property Pool](#random-properties) · [Prototype](#working-method) · [Provenance Stamp](#packages) · [Provisioning Profile](#sharding-and-transfer) · [Public Note](#guilds)
**P** [Package](#packages) · [Package API](#packages) · [Package Cast Range](#packages) · [Package Config](#packages) · [Package Creature Range](#packages) · [Package Delta](#packages) · [Package Event](#runtime-scripts) · [Package EventAI Range](#packages) · [Package Fixture](#packages) · [Package Gameobject Range](#packages) · [Package Globals Range](#packages) · [Package Gossip Range](#packages) · [Package Identifier Range](#packages) · [Package Import](#packages) · [Package Inventory](#packages) · [Package Item Range](#packages) · [Package Loot Range](#packages) · [Package Quest Range](#packages) · [Package Script Range](#packages) · [Package Source](#packages) · [Package Spell](#packages) · [Package Spell Metadata Range](#packages) · [Package Spell Range](#packages) · [Package Teardown](#packages) · [Package Trainer Range](#packages) · [Package-owned Account](#accounts-characters-and-sessions) · [Party](#sharding-and-transfer) · [Party Partition](#sharding-and-transfer) · [Patrol Pause](#creature-ai) · [Petition](#guilds) · [Petitioner](#guilds) · [Pre-auth I/O Deadline](#accounts-characters-and-sessions) · [Proc](#procs) · [Proficiency](#gateway-and-module) · [Property Pool](#random-properties) · [Protocol Family](#working-method) · [Prototype](#working-method) · [Provenance Stamp](#packages) · [Provisioning Profile](#sharding-and-transfer) · [Public Note](#guilds)

**R** [Raid](#sharding-and-transfer) · [Raid Quest](#sharding-and-transfer) · [Raid Slot](#sharding-and-transfer) · [Random Property](#random-properties) · [Ranged Posture](#creature-ai) · [Rank Rights](#guilds) · [Ready Check](#sharding-and-transfer) · [Realm](#realm-topology) · [Realm Chat Line](#chat) · [Realm Clock](#world-clock-and-weather) · [Realm Presence](#chat) · [Realm-core](#realm-topology) · [Recorded Revision](#packages) · [Recovery Attempt](#sharding-and-transfer) · [Recovery Scan](#sharding-and-transfer) · [Reference Datascript](#packages) · [Reference Package](#packages) · [Refusal](#gateway-and-module) · [Relay](#gateway-and-module) · [Relay Definition](#realm-topology) · [Relay Run](#realm-topology) · [Returned Mail](#mail) · [Reward Letter](#mail) · [Roster Revision](#sharding-and-transfer) · [Roster Revision Relay](#sharding-and-transfer) · [Route Path](#procs) · [Route Step](#procs) · [Rule State](#creature-ai) · [Runtime Script](#runtime-scripts) · [Runtime Script Host](#runtime-scripts) · [Runtime Script Toolchain](#packages)

**S** [Script Answer](#runtime-scripts) · [Script Artifact](#packages) · [Script Diagnostic](#runtime-scripts) · [Script Directive](#packages) · [Seam](#working-method) · [Seeker](#meeting-stones) · [Self-Resurrection Option](#death-and-resurrection) · [Service Reconciliation](#realm-topology) · [Session](#accounts-characters-and-sessions) · [Session Expiry](#accounts-characters-and-sessions) · [SessionActor](#accounts-characters-and-sessions) · [Sessionless Action Consent](#sharding-and-transfer) · [Settlement](#auctions) · [Shard](#realm-topology) · [Shard Boundary](#sharding-and-transfer) · [Shard Map](#sharding-and-transfer) · [Signature](#guilds) · [Solo Target Claim](#sharding-and-transfer) · [Soulstone](#death-and-resurrection) · [Spatial Claim](#packages) · [Speaker Facts](#chat) · [Spec](#working-method) · [Spell Cast Event Kind](#procs) · [Staged Effect](#runtime-scripts) · [Standalone Supervisor](#realm-topology) · [Stat Kind](#random-properties) · [Stone Add](#meeting-stones) · [Store](#working-method) · [Strategy](#sharding-and-transfer) · [Subgroup](#sharding-and-transfer) · [Suffix](#random-properties)
**S** [Script Answer](#runtime-scripts) · [Script Artifact](#packages) · [Script Diagnostic](#runtime-scripts) · [Script Directive](#packages) · [Seam](#working-method) · [Seeker](#meeting-stones) · [Self-Resurrection Option](#death-and-resurrection) · [Service Reconciliation](#realm-topology) · [Session](#accounts-characters-and-sessions) · [Session Expiry](#accounts-characters-and-sessions) · [SessionActor](#accounts-characters-and-sessions) · [Sessionless Action Consent](#sharding-and-transfer) · [Settlement](#auctions) · [Shard](#realm-topology) · [Shard Boundary](#sharding-and-transfer) · [Shard Map](#sharding-and-transfer) · [Signature](#guilds) · [Solo Target Claim](#sharding-and-transfer) · [Soulstone](#death-and-resurrection) · [Spatial Claim](#packages) · [Speaker Facts](#chat) · [Spec](#working-method) · [Speech](#chat) · [Spell Cast Event Kind](#procs) · [Staged Effect](#runtime-scripts) · [Standalone Supervisor](#realm-topology) · [Stat Kind](#random-properties) · [Stone Add](#meeting-stones) · [Store](#working-method) · [Strategy](#sharding-and-transfer) · [Subgroup](#sharding-and-transfer) · [Suffix](#random-properties)

**T** [Tabard Designer](#guilds) · [Target Icon](#sharding-and-transfer) · [Ticket](#working-method) · [Tracer](#working-method) · [Trade Commit](#trading) · [Trade Session](#trading) · [Transfer](#sharding-and-transfer) · [Transfer Intent](#sharding-and-transfer) · [Triggered Cast](#procs) · [Trust Review](#packages)

Expand Down Expand Up @@ -495,6 +495,11 @@ _Avoid_: chat type (in new names)
The speaker's race and chat tag, read by the Gateway on the Home Shard and conveyed in the Durable
Request. Realm-core holds no Characters.

**Speech**:
A line a Character speaks to the Characters near it: a say, yell or `/e` line, or a text emote. Its
Durable Request goes to the speaker's Home Shard, and a Relay delivers it to listeners in range.
_Avoid_: local chat, proximity chat

**Realm Presence**:
The Gateway's realm-wide read of one Character: Whereabouts, session online, race, class, level and
zone, from whichever World Shard holds it. Guild rosters, friends, `/who`, whisper and Member Stats
Expand Down Expand Up @@ -1223,6 +1228,10 @@ _Avoid_: client patch, distribution, release bundle, player package
**Seam**:
The interface where session or protocol handling hands work to durable state, expressed as a trait so tests can substitute the far side. Not the Shard Boundary, not any arbitrary trait.

**Protocol Family**:
A group of client opcodes one Gateway handler owns, with the Store trait that handler calls (for example the vendor family and `VendorActionStore`). `WorldStore` is the umbrella over every family; a handler takes only its family's Store.
_Avoid_: family (unqualified), domain

**Store**:
The durable side of a Seam: the trait a handler calls for Durable Reads and Durable Requests. The Coordinator implements it in production, a Fake in tests.
_Avoid_: repository, adapter, port, backend, service
Expand Down
9 changes: 9 additions & 0 deletions CORE_TERMS.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ The Gateway's subscribed connection per shard, authenticated with the Owner Toke
Gateway code that turns a table change into a client message.
_Avoid_: forwarder, pusher

**Speech**:
A line a Character speaks to the Characters near it: a say, yell or `/e` line, or a text emote. Its
Durable Request goes to the speaker's Home Shard, and a Relay delivers it to listeners in range.
_Avoid_: local chat, proximity chat

**AOI**:
The area of interest that decides which entities a World Session sees.
_Avoid_: visibility set, interest radius
Expand Down Expand Up @@ -114,6 +119,10 @@ _Avoid_: SDK, plugin API, public API, allowlist
**Seam**:
The interface where session or protocol handling hands work to durable state, expressed as a trait so tests can substitute the far side. Not the Shard Boundary, not any arbitrary trait.

**Protocol Family**:
A group of client opcodes one Gateway handler owns, with the Store trait that handler calls (for example the vendor family and `VendorActionStore`). `WorldStore` is the umbrella over every family; a handler takes only its family's Store.
_Avoid_: family (unqualified), domain

**Store**:
The durable side of a Seam: the trait a handler calls for Durable Reads and Durable Requests. The Coordinator implements it in production, a Fake in tests.
_Avoid_: repository, adapter, port, backend, service
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ This is the sharding model in full: the hierarchy, routing, and transfer.
### 6.2 Resolving "which database owns this position"

```
world entry: WorldStore::settle_home_shard (gateway/src/stdb/world_store.rs)
world entry: ShardRoutingStore::settle_home_shard (gateway/src/stdb/store/shard_routing.rs)
1. realm-core's character→shard index — a HINT, confirmed against the shard that
actually holds the row, self-healing on disagreement
2. the (map_id, instance_id) shard map — shard_for, with instance-pool routing
Expand Down
7 changes: 4 additions & 3 deletions docs/recipes/add-an-opcode.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,10 @@ family's dispatcher and skip to step 10.

## 8. Join the family to `WorldStore`

- File: `gateway/src/world/store.rs`. Add the trait to the `WorldStore` supertrait list.
- File: `gateway/src/world/tests.rs`. Implement the trait for `InMemoryStore`, the shared Fake that
implements every family.
- File: `gateway/src/world/store.rs`. Add the trait to the `WorldStore` supertrait list and to the
bounds of its blanket impl.
- File: `gateway/src/world/test_support/world_fake/<family>.rs`. Implement the trait for `WorldFake`,
the shared Fake that implements every family, and keep the family's state in its own struct there.

## 9. Link the dispatcher into the chain

Expand Down
2 changes: 1 addition & 1 deletion gateway/src/codec/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1315,7 +1315,7 @@ fn raw_movement_relay_is_byte_identical_to_the_typed_path() {
const GUIDS: &[u64] = &[1, 0x0100, u64::MAX, 0, 0x0000_00F3_0000_0042];
let mut compared = 0usize;
for (label, info) in movement_info_cases() {
// The bytes the module stores are produced by exactly this call (`WorldStore::movement_update`).
// The bytes the module stores are produced by exactly this call (`SessionStore::movement_update`).
let stored = movement_info_to_bytes(&info).unwrap();
for &opcode in movement_opcodes::SLICE_MOVE_OPCODES {
for &guid in GUIDS {
Expand Down
161 changes: 2 additions & 159 deletions gateway/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,8 +331,8 @@ impl ShardMap {
/// Every distinct WORLD SHARD this map can resolve to, default first. These are the databases
/// that hold characters, so these — and only these — are what a character-location probe walks.
/// Realm-core is deliberately not here: it holds no characters, so probing it is pure waste.
/// ⚠ **Default stays FIRST.** `resolve_home_shard`'s fallback probe walks the connected set in
/// this order and must try the default database before any rule shard.
/// ⚠ **Default stays FIRST.** The fallback census walks the connected set in this order and
/// must try the default database before any rule shard.
pub fn shards(&self) -> Vec<String> {
let mut dbs = vec![self.default_db.clone()];
for r in &self.rules {
Expand Down Expand Up @@ -533,66 +533,6 @@ impl InstanceHosting {
}
}

/// Where the realm believes a character lives, and whether the realm-core index needs correcting.
#[derive(Clone, Debug, PartialEq)]
pub struct HomeShard {
/// The database that owns the character.
pub db: String,
/// The `(map_id, instance_id)` that resolved it — the value written back on a heal.
pub location: (u32, u64),
/// The realm-core index disagreed (or had nothing) — write `location` back to it.
pub heal: bool,
}

/// Resolve which database owns `character_guid`, from the realm-core index hint plus a probe of the
/// connected world shards. Pure, so the whole self-heal rule is unit-testable without a
/// node — deliberately, because an earlier shard-routing change shipped a bug hidden by a mock that
/// hardcoded exactly the answer under test.
///
/// - `hint` — the realm-core `game_character_shard` entry, or `None` when the index has no idea.
/// - `connected` — the world shards that actually connected, DEFAULT FIRST (probe order).
/// - `probe(db)` — "does `db` hold this character, and at what location" (the coordinator's
/// `character_location` against that shard's cache; an in-memory lookup, not a round trip).
///
/// The rule: **the index is a hint, the row is the truth.** A hint is accepted only when the shard it
/// names actually holds the character; anything else falls through to walking the shards, and the
/// answer is marked for write-back. With ONE connected shard this degenerates to "probe the default
/// shard and resolve its location through the map" — i.e. the original shard-routing behavior, byte
/// for byte.
pub fn resolve_home_shard(
map: &ShardMap,
connected: &[String],
hint: Option<(u32, u64)>,
probe: impl Fn(&str) -> Option<(u32, u64)>,
) -> Option<HomeShard> {
let is_connected = |d: &str| connected.iter().any(|c| c == d);
let settle = |location: (u32, u64)| HomeShard {
db: map
.resolve_connected(location.0, location.1, is_connected)
.to_string(),
location,
heal: hint != Some(location),
};

// 1. Fast path: trust the hint only far enough to know WHICH shard to ask, then confirm with
// that shard's own row. A wrong hint therefore costs one extra probe, never a wrong route.
if let Some((map_id, instance_id)) = hint {
let hinted = map.resolve_connected(map_id, instance_id, is_connected);
if let Some(location) = probe(hinted) {
return Some(settle(location));
}
log::info!(
"realm-core index points at map {map_id}/instance {instance_id} (shard {hinted}), but \
that shard does not hold the character — probing; the index will self-heal"
);
}

// 2. Fallback probe (the self-heal): ask every connected shard, default first, and route by the
// location the owning shard reports — exactly how the shard map routes, just sourced by
// probe rather than by assuming the default shard still holds the row.
connected.iter().find_map(|db| probe(db)).map(settle)
}

/// `<map_id|*>[:<bucket|*>]=<db>` → a rule, or `None` if it doesn't parse.
fn parse_shard_rule(rule: &str) -> Option<ShardRule> {
let (loc, db) = rule.split_once('=')?;
Expand Down Expand Up @@ -1548,103 +1488,6 @@ mod shard_map_tests {
assert_eq!(m.realm_core_db(), Some("lyracore-realm"));
}

#[test]
fn one_shard_resolves_home_from_the_probe_alone_exactly_like_before_realm_core() {
// Backward compatibility, stated as a test: single shard, no index entry → "ask the default
// shard where the character is, resolve that through the map" verbatim.
let m = ShardMap::parse("world", "");
let connected = vec!["world".to_string()];
let got = resolve_home_shard(&m, &connected, None, |db| (db == "world").then_some((0, 0)));
assert_eq!(
got,
Some(HomeShard {
db: "world".into(),
location: (0, 0),
heal: true
})
);
// Nobody holds the character → no answer at all (the caller keeps its current handle).
assert_eq!(resolve_home_shard(&m, &connected, None, |_| None), None);
}

#[test]
fn a_matching_index_entry_routes_without_a_heal() {
let m = ShardMap::parse("world", "389:*=instances");
let connected = vec!["world".to_string(), "instances".to_string()];
let got = resolve_home_shard(&m, &connected, Some((389, 12)), |db| {
(db == "instances").then_some((389, 12))
});
assert_eq!(
got,
Some(HomeShard {
db: "instances".into(),
location: (389, 12),
heal: false
}),
"an index entry the owning shard confirms is used as-is, with no write-back"
);
}

#[test]
fn a_deliberately_stale_index_entry_self_heals_via_the_fallback_probe() {
// The index says the character is in a Deadmines instance; it actually walked out and
// is standing in the open world. The hinted shard doesn't hold it, so the probe walks the
// shards, finds it on `world`, routes there, and flags the entry for correction.
let m = ShardMap::parse("world", "389:*=instances");
let connected = vec!["world".to_string(), "instances".to_string()];
let got = resolve_home_shard(&m, &connected, Some((389, 12)), |db| {
(db == "world").then_some((0, 0))
});
assert_eq!(
got,
Some(HomeShard {
db: "world".into(),
location: (0, 0),
heal: true
}),
"a stale entry must route by the row that actually exists, and mark itself for repair"
);
}

#[test]
fn an_index_entry_naming_a_shard_that_lies_about_the_location_still_routes_by_the_row() {
// The hinted shard DOES hold the character, but at a location that belongs to a different
// shard (mid-transfer, or a shard-map edit since the entry was written). The location the
// row reports is the authority for routing, and the entry gets corrected.
let m = ShardMap::parse("world", "389:*=instances");
let connected = vec!["world".to_string(), "instances".to_string()];
let got = resolve_home_shard(&m, &connected, Some((0, 0)), |db| {
(db == "world").then_some((389, 3))
});
assert_eq!(
got,
Some(HomeShard {
db: "instances".into(),
location: (389, 3),
heal: true
})
);
}

#[test]
fn an_index_entry_pointing_at_a_disconnected_shard_degrades_to_the_default_then_probes() {
// `resolve_connected` sends the hint to the default database when the named shard is down;
// if the default doesn't hold the character either, the probe still walks the rest.
let m = ShardMap::parse("world", "389:*=instances, 1:*=pool-b");
let connected = vec!["world".to_string(), "instances".to_string()]; // pool-b never connected
let got = resolve_home_shard(&m, &connected, Some((1, 0)), |db| {
(db == "instances").then_some((389, 7))
});
assert_eq!(
got,
Some(HomeShard {
db: "instances".into(),
location: (389, 7),
heal: true
})
);
}

// ==========================================================================================
// The SINGLE-DATABASE realm, and the parser's totality
// ==========================================================================================
Expand Down
Loading
Loading