Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
cb7756f
refactor(gateway): classify durable failures and type the Actor
Mathih13 Oct 10, 2026
6e13590
refactor(gateway): move each Store family's Coordinator code into its…
Mathih13 Oct 10, 2026
a9f116b
refactor(gateway): collapse the combat Store to one hop
Mathih13 Oct 10, 2026
378ab0a
refactor(gateway): collapse the duel Store to one hop
Mathih13 Oct 10, 2026
3a929b8
refactor(gateway): collapse the trade Store to one hop
Mathih13 Oct 10, 2026
e466acf
refactor(gateway): collapse the death Store to one hop
Mathih13 Oct 10, 2026
c46586c
refactor(gateway): collapse the loot window Store to one hop
Mathih13 Oct 10, 2026
21aba98
refactor(gateway): collapse the loot roll Store to one hop
Mathih13 Oct 10, 2026
cfc8a2f
refactor(gateway): resolve the loot and death Actor once in handle_loot
Mathih13 Oct 10, 2026
316a883
refactor(gateway): melee Store takes an Actor and classifies durable …
Mathih13 Oct 10, 2026
0152aed
refactor(gateway): quest Store takes an Actor and classifies durable …
Mathih13 Oct 10, 2026
2ccbbd2
refactor(gateway): collapse the vendor family to one hop
Mathih13 Oct 10, 2026
36ff22a
refactor(gateway): collapse the auction family to one hop
Mathih13 Oct 10, 2026
3ff58cb
refactor(gateway): chat takes an Actor and classifies by Transport Loss
Mathih13 Oct 10, 2026
4b63b83
refactor(gateway): channel takes an Actor and classifies by Transport…
Mathih13 Oct 10, 2026
bc3dc7d
refactor(gateway): guild fee takes an Actor
Mathih13 Oct 10, 2026
4e97cea
refactor(gateway): guild takes an Actor and classifies by Transport Loss
Mathih13 Oct 10, 2026
c6d68e6
refactor(gateway): item Store takes an Actor
Mathih13 Oct 10, 2026
ce7b0cf
refactor(gateway): cast Store takes an Actor and classifies durable f…
Mathih13 Oct 10, 2026
23ac15f
refactor(gateway): session, character, social and party Stores act as…
Mathih13 Oct 10, 2026
0ab8ae2
refactor(gateway): the Transfer Store freezes the Character as an Actor
Mathih13 Oct 10, 2026
2cc4bdf
refactor(gateway): mail acts as an Actor and ends the session on a Tr…
Mathih13 Oct 10, 2026
989712d
Merge remote-tracking branch 'origin/p4/family-2' into refactor/coord…
Mathih13 Oct 10, 2026
8bc412d
Merge remote-tracking branch 'origin/p4/family-3' into refactor/coord…
Mathih13 Oct 10, 2026
d93084d
Merge remote-tracking branch 'origin/p4/family-4' into refactor/coord…
Mathih13 Oct 10, 2026
f09f262
Merge remote-tracking branch 'origin/p4/family-6' into refactor/coord…
Mathih13 Oct 10, 2026
9f2eb66
refactor(gateway): inline the weather and member stats Coordinator reads
Mathih13 Oct 10, 2026
9a4bce2
refactor(gateway): bank Store takes an Actor and classifies failures
Mathih13 Oct 10, 2026
6c87bad
refactor(gateway): meeting stone Store takes an Actor and classifies …
Mathih13 Oct 10, 2026
a47ddb8
refactor(gateway): taxi Store takes an Actor and classifies failures
Mathih13 Oct 10, 2026
e23543a
refactor(gateway): trainer Store takes an Actor and classifies failures
Mathih13 Oct 10, 2026
f745b6e
refactor(gateway): npc Store takes an Actor and classifies failures
Mathih13 Oct 10, 2026
5b9e767
refactor(gateway): speech Store takes an Actor and classifies failures
Mathih13 Oct 10, 2026
e7b9732
Merge remote-tracking branch 'origin/p4/family-5' into refactor/coord…
Mathih13 Oct 10, 2026
bc44b62
refactor(gateway): drop the last text classifier after merging the fa…
Mathih13 Oct 10, 2026
15363b5
refactor(gateway): finish the one-hop Coordinator
Mathih13 Oct 10, 2026
ab718dc
Merge origin/main into refactor/coordinator-one-hop
Mathih13 Oct 10, 2026
e09a41e
fix(gateway): keep taxi failures fatal, as before the one-hop change
Mathih13 Oct 10, 2026
f286099
fix(gateway): an unknown Realm Presence no longer ends the World Session
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
17 changes: 13 additions & 4 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ A World of Warcraft 1.12.1 server. Game state lives in SpacetimeDB; the gateway

**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)
**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) · [Transport Loss](#gateway-and-module) · [Triggered Cast](#procs) · [Trust Review](#packages)

**U** [UI Transform](#client-content)

Expand Down Expand Up @@ -178,9 +178,15 @@ _Avoid_: mutation, write, reducer call (in gateway prose)
A read of Module state through the Coordinator.

**Refusal**:
A Gate saying no to a Durable Request. An expected gameplay outcome, not a transport failure.
A Gate saying no to a Durable Request. An expected gameplay outcome, not a Transport Loss.
_Avoid_: reject, deny, error (for gameplay refusals)

**Transport Loss**:
A failed Durable Read or Durable Request that is not a Refusal: the transport dropped, the call
timed out, or the send failed. Its durable outcome is unknown, so it ends the World Session and the
client logs in again from durable state.
_Avoid_: transport failure, fatal error, disconnect

### Accounts, characters and sessions

**Account**:
Expand Down Expand Up @@ -208,7 +214,9 @@ A guid-owned player entity.
_Avoid_: player (as a noun in code)

**Actor**:
The guid a Gateway Verb acts as.
The Character a Gateway Verb acts as. In the Gateway it is the `Actor` type, a nonzero guid.
`Actor::new` returns `None` for guid 0, so no request acts as guid 0. The Coordinator signs an Actor
into a SessionActor.

**Session**:
The Module's record that an Account is logged in on a Character.
Expand Down Expand Up @@ -510,7 +518,8 @@ _Avoid_: online status, presence cache
Realm Presence's own state for where a Character is: in world (with a live entity and an Away
Status), in transit between two places (a pending Transfer, or a Shard's own row reading online
with no entity there), or offline. A negative Whereabouts, offline or no Character found at all,
needs every configured World Shard to vouch that none of them is hiding the Character.
needs every configured World Shard to vouch that none of them is hiding the Character. When one
cannot, the Whereabouts is unknown: another Shard's health, not a Transport Loss.
_Avoid_: presence state, location status

**Away Status**:
Expand Down
12 changes: 10 additions & 2 deletions CORE_TERMS.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,24 @@ The identity that publishes and owns the shards, and the only caller of Gateway
A `gw_*` reducer the Operator calls on a character's behalf, with the Actor named by guid.

**Actor**:
The guid a Gateway Verb acts as.
The Character a Gateway Verb acts as. In the Gateway it is the `Actor` type, a nonzero guid.
`Actor::new` returns `None` for guid 0, so no request acts as guid 0. The Coordinator signs an Actor
into the `SessionActor` a reducer receives.

**Gate**:
A rule that refuses a request. Gates live in the Module, except the realm-wide reads only the Gateway can perform (presence, name resolution, loot-roll fan-out).
_Avoid_: validation, guard

**Refusal**:
A Gate saying no to a Durable Request. An expected gameplay outcome, not a transport failure.
A Gate saying no to a Durable Request. An expected gameplay outcome, not a Transport Loss.
_Avoid_: reject, deny, error (for gameplay refusals)

**Transport Loss**:
A failed Durable Read or Durable Request that is not a Refusal: the transport dropped, the call
timed out, or the send failed. Its durable outcome is unknown, so it ends the World Session and the
client logs in again from durable state.
_Avoid_: transport failure, fatal error, disconnect

**Durable Request**:
A reducer call the Gateway makes that changes Module state.
_Avoid_: mutation, write, reducer call (in gateway prose)
Expand Down
7 changes: 6 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ routing fact; module game logic never reads one, and an architecture test fails
character, and the nine-boundary Transfer driver. See §6.
- **The subscription plane** — the AOI tracker and every `on_insert` relay that turns a table delta
into an SMSG. See §5.
- **Durable Requests in one hop.** A family handler in `gateway/src/world/handlers/` resolves its
session facts to an `Actor` once and calls its Store trait. The trait's `Coordinator` impl in
`gateway/src/stdb/store/<family>.rs` calls the reducer directly. When the call fails,
`classify` in `gateway/src/stdb/connection.rs` decides. A Refusal gets the family's answer and
the session continues. Every other failure is a Transport Loss and ends the World Session.

### 2.3 The honest exception: gates that live in the gateway

Expand All @@ -107,7 +112,7 @@ are answered by the gateway, which is the only component that can see the whole
| whisper target resolution by name, realm-wide, plus the ignore verdict and the Auto-Reply | same | `presence.rs` (`resolve_all_by_name`, `of`, `auto_reply`), `gateway/src/world/whisper.rs` (`target_facts`, `ignored_anywhere`) |
| `CMSG_NAME_QUERY` resolution | same | `presence.rs` (`character_anywhere`) |
| friend/ignore add target resolution by name, realm-wide, plus the friend's race for the Enemy Gate. This is the existence check the Module no longer performs, since Realm-core holds no Character rows for it to check | same | `presence.rs` (`resolve_by_name`, `of`), `gateway/src/world/social.rs` (`resolve_add_contact`) |
| Speaker Facts: the speaker's race and chat tag for every Realm Chat Line and Chat Channel op | Realm-core holds no Characters and no live entities | `gateway/src/world/handlers/chat.rs` (`speaker_facts`), `stdb/reads/chat.rs` |
| Speaker Facts: the speaker's race and chat tag for every Realm Chat Line and Chat Channel op | Realm-core holds no Characters and no live entities | `gateway/src/world/handlers/chat.rs` (`speaker_facts`), `stdb/store/chat.rs` |
| Seeker facts: the race and class of the Character that uses a Meeting Stone, or of every member of the Party it leads, and the class of a Character that accepts an invite into a queued Party. Also whether the client can name the stone's dungeon area, checked before any state changes | Realm-core holds no Characters, and only the Gateway knows which areas the client protocol encodes | `gateway/src/world/handlers/meeting_stone.rs` (`facts_for_join`, the `Area` check in `join`), `gateway/src/world/party.rs` (`AcceptorFacts`) |
| channel moderation target names, realm-wide, plus whether the target ignores the actor for an invite | same | `gateway/src/world/handlers/channel.rs` (`resolve_online_character`, `ignores`) |
| the per-listener ignore filter on an ignorable Realm Chat Line | the ignore list is on each listener's Home Shard; the line is on Realm-core | `gateway/src/stdb/world_view.rs` (`realm_chat_appeared`, `Viewer::ignores`) |
Expand Down
51 changes: 32 additions & 19 deletions docs/recipes/add-an-opcode.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ The Gateway carries the client's intent. The Module decides.
- A Realm-core operation is a `realm_<verb>` reducer and takes the realm-wide facts the Gateway
read.
- A Refusal returns `Err` with a tag the Gateway can parse back into a typed Refusal. The Meeting
Stone Queue keeps that type in `lyracore_shared::meeting_stone`. A transport failure stays an
error, so the two never mix.
Stone Queue keeps that type in `lyracore_shared::meeting_stone`. The Gateway's `classify` treats
every other error as a Transport Loss, so the two never mix.
- Declare a new module file in `module/src/lib.rs`.

## 3. Store what the Relay or a Durable Read needs
Expand All @@ -57,28 +57,34 @@ Required for every new table and every new reducer.
`coordinator_queries` in `gateway/src/stdb/connection.rs`, then add a `parity_test!` and the table
name to `MANIFEST_TABLES` in `gateway/tests/schema_parity.rs`.

## 5. Call the reducer from the Coordinator
## 5. Call the reducer from the Store trait method

File: `gateway/src/stdb/reducers.rs`. Add an `impl Coordinator` method that makes the Durable
Request:
File: `gateway/src/stdb/store/<family>.rs`, declared in `gateway/src/stdb/store/mod.rs`. It holds
`impl <Family>ActionStore for Coordinator`. Each trait method makes its Durable Request itself, as
the `Actor` the handler resolved:

```rust
let coord = self.0.call_pipe();
call_reducer!(
coord.conn.reducers,
"gw_admit_meeting_stone",
gw_admit_meeting_stone_then(self.session_actor(actor_guid), go_guid)
)
fn admit_meeting_stone(&self, actor: Actor, go_guid: u64) -> Result<MeetingStoneOutcome> {
meeting_stone_outcome(call_reducer!(
self.0.call_pipe().conn.reducers,
"gw_admit_meeting_stone",
gw_admit_meeting_stone_then(self.session_actor(actor), go_guid)
))
}
```

A Realm-core call goes through `self.realm_core()?` first. Turn a parsed Refusal into a typed
outcome with `reducer_refusal_reason`, and return every other error unchanged.
Do not add an inherent `impl Coordinator` method for the trait method to forward to. Keep one only
when a Relay, logon or another family calls it too. A Realm-core call goes through
`self.realm_core()?` first. A Durable Request the Gateway makes for the World Session, with no
client acting, uses `self.owner_actor()`. A family with a typed Refusal parses it with
`reducer_refusal_reason` and returns every other error unchanged.

## 6. Add the Durable Reads

File: `gateway/src/stdb/reads/<family>.rs`, declared in `gateway/src/stdb/reads/mod.rs`. Each read
is an `impl Coordinator` method that finds rows in the Coordinator cache by a unique index. The SDK
cache has no other index, and a whole-table `iter()` holds the lock the pump needs.
File: `gateway/src/stdb/store/<family>.rs`. Write the read in the trait method. A read that a Relay
or another family also calls is an `impl Coordinator` method in `gateway/src/stdb/reads/`. Find rows
in the Coordinator cache by a unique index. The SDK cache has no other index, and a whole-table
`iter()` holds the lock the pump needs.

## 7. Write the family dispatcher

Expand All @@ -87,8 +93,13 @@ a `pub(crate) use` of its public items. For an opcode in an existing family, add
family's dispatcher and skip to step 10.

- A Store trait, `<Family>ActionStore`. Each method's doc says whether it is a Durable Request or a
Durable Read and on which Shard.
- `impl <Family>ActionStore for crate::stdb::Coordinator`, each method one call to step 5 or 6.
Durable Read and on which Shard. A method that acts for a Character takes `actor: Actor`. Steps 5
and 6 implement it.
- The dispatcher resolves the session's Character to `Option<Actor>` once, with `Actor::new`. When
it is `None`, make no Durable Request. The client usually gets no answer.
- When a Store call fails, match on `classify(&error)`. A `DurableFailure::Refusal { reason }` gets
the family's answer to the client and the session continues. A `DurableFailure::TransportLoss`
returns the `Err`, which ends the World Session.
- A player struct with the session facts the family needs, and an outcome enum with
`Handled { outbound }` and `PassThrough(msg)`.
- `dispatch_<family>_action(store, player, msg)` matches the family's `ClientOpcodeMessage` variants
Expand Down Expand Up @@ -132,7 +143,9 @@ File: `gateway/src/stdb/world_view.rs`.

## 12. Test each seam

- The dispatcher, through its local Fake, in the family file.
- The dispatcher, through its local Fake, in the family file. One Fake answers
`ReducerCallError::refused` and the client gets the Refusal answer. One answers
`ReducerCallError::transport_lost` and the dispatcher returns `Err`.
- The packet bytes, in the codec file.
- The Relay's recipient choice and packet, in the `world_view.rs` test modules.
- The Module rule, in a durable test `module/tests/<family>.rs`. CI picks up a new target with no
Expand Down
Loading
Loading