diff --git a/.gitignore b/.gitignore index 233469761..b5a77ab6b 100644 --- a/.gitignore +++ b/.gitignore @@ -28,8 +28,8 @@ gateway/gateway-stderr.log dhat-heap.json __pycache__/ -# cargo-mutants report output (#380). Regenerated on every run; the CI job uploads it as an -# artifact instead. `.cargo/mutants.toml` is the configuration and IS checked in. +# cargo-mutants report output. Regenerated on every run, so it is never committed. This repository +# carries no mutants configuration file; the maintainers run cargo-mutants from their own repository. /mutants.out/ /mutants.out.old/ .scratch/ diff --git a/.wire-harness-rev b/.wire-harness-rev index c131b5497..08277a9fd 100644 --- a/.wire-harness-rev +++ b/.wire-harness-rev @@ -7,7 +7,7 @@ # any other 5875 core?" unanswerable. # # ONE line: ` `, and BOTH halves are load-bearing. -# * the TAG is what `scripts/wire-harness.sh` checks out — a release, not a branch. There is +# * the TAG is what `lyracore dev smoke` checks out — a release, not a branch. There is # deliberately no way to say "main" here. # * the SHA is what it then VERIFIES the checkout resolved to. A git tag is a mutable ref; a # re-pointed tag would otherwise change what this checkout builds without changing this file, diff --git a/CHANGELOG.md b/CHANGELOG.md index 5262f6f1a..3c308f609 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,14 @@ # Changelog -All notable changes to LyraCore are recorded here. The format follows -[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow -[Semantic Versioning](https://semver.org/spec/v2.0.0.html) — with the caveat that a `0.x` alpha -makes no compatibility promise at all: schemas, environment variables, CLI flags, and database -names may change between pre-releases without a deprecation period. +This file records release-level changes only. It is **not** a running log of every merge, and it is +not maintained as one: day-to-day changes are the +[merged pull requests](https://github.com/LyraCoreProject/LyraCore/pulls?q=is%3Apr+is%3Amerged), +which are the honest history. Read this file for what a release means, and the PR list for what +happened. + +Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html), with the caveat that a +`0.x` alpha makes no compatibility promise at all: schemas, environment variables, CLI flags and +database names may change between pre-releases without a deprecation period. This file starts at the first public release. The work before it happened in a private repository and is not reconstructed here. diff --git a/Cargo.toml b/Cargo.toml index ab74ae8e3..d5e3b7dfb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,11 +7,10 @@ members = [ "gateway", "importer", ] -# NOT a member any more (#246): the headless build-5875 wire client lives in its own repository, -# LyraCoreProject/wire-harness, and is consumed as a PINNED RELEASE — see `.wire-harness-rev`, -# `scripts/wire-harness.sh` and `adapters/lyracore/run-suite.sh`. It was never a server crate: it -# is a server-AGNOSTIC protocol tool, and a workspace membership is exactly the coupling that made -# "does this run against any other 5875 core?" unanswerable. +# The headless build-5875 wire client is deliberately not a member. It lives in its own repository, +# LyraCoreProject/wire-harness, and is consumed as a pinned release; `.wire-harness-rev` holds the +# pin and `lyracore dev smoke` resolves it. It is a server-agnostic protocol tool, and a workspace +# membership was the coupling that made "does this run against any other 5875 core?" unanswerable. # The SpacetimeDB module compiles to wasm and pulls its own (large) dependency tree; # keep workspace-wide settings minimal so the module and the native gateway can diverge. @@ -19,28 +18,15 @@ members = [ [workspace.package] edition = "2021" license = "MIT OR Apache-2.0" -# Amended twice, both by empirical build attempts rather than by rule: -# - #231 moved this from 1.80 to 1.85: SpacetimeDB 2.5.0's crates declare `edition = "2024"`, -# which needs Cargo >=1.85 just to PARSE their manifests, so 1.80 was never buildable. -# - #229's CI work found 1.85 *itself* still isn't buildable: `spacetimedb`/`spacetimedb-sdk` -# 2.5.0 (and several of their own transitive deps — icu_*, darling, image, wow-mpq/-adt/-blp/ -# -m2/-wmo) each declare a `rust-version` field of their own, and the highest of those is -# `spacetimedb`/`spacetimedb-sdk`'s **1.93.0** — `cargo +1.85.0 check --workspace` fails -# outright with "rustc 1.85.0 is not supported" for exactly that reason, before a single line -# of this repo's own code is even considered. -# - re-checked at the 2.5.0 -> 2.7.1 bump: every `spacetimedb-*` crate still declares -# `rust-version = "1.93.0"` and `edition = "2024"`, so the floor below did not move. -# Net result: today's REAL floor is 1.93.0, identical to the pinned dev toolchain below — there is -# no daylight between "MSRV" and "the toolchain everyone already builds with" to test separately. -# CI's `msrv-consistency` job (#229) asserts these two numbers stay equal instead of pretending a -# lower, unbuildable one is real; if a future SpacetimeDB release relaxes its own `rust-version`, -# lowering this one again needs the same empirical check, not an assumption. +# Two facts set this floor, both re-checked at the 2.7.1 bump: every `spacetimedb-*` crate declares +# `rust-version = "1.93.0"`, and they declare `edition = "2024"`, which Cargo below 1.85 cannot even +# parse. So the real MSRV equals the toolchain pinned in `rust-toolchain.toml` and there is nothing +# lower to test. Keep the two numbers equal. Lowering this needs a build attempt, not an assumption. rust-version = "1.93.0" repository = "https://github.com/LyraCoreProject/LyraCore" homepage = "https://github.com/LyraCoreProject/LyraCore" -# Every crate in this workspace is INTERNAL to the LyraCore server. None of them is a library -# anyone is meant to depend on from crates.io, and #239's alpha explicitly does not publish -# either repository — so the whole workspace is publish-gated here rather than crate by crate. +# Every crate here is internal to the LyraCore server. None is a library anyone should depend on from +# crates.io, so the whole workspace is publish-gated once rather than crate by crate. publish = false [workspace.dependencies] diff --git a/README.md b/README.md index 629f2e6ca..d841169d4 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,9 @@ Point your client's `realmlist.wtf` at `127.0.0.1` and play. ## Can I use my existing client? -A client never speaks to SpacetimeDB — it speaks SRP6 and WoW opcodes — so the gateway is translator and every mutation the world can undergo is a reducer call. The -realm is sharded across several databases running the *same* wasm; `dev up` brings up a sharded -fixture with a live seam you can walk across mid-session. +Yes. An unmodified 1.12.1 client, build 5875, is the only client this project supports. Nothing is +patched, no launcher is replaced, and no addon is required. Point its `realmlist.wtf` at your realm +and log in. Other 1.12.x builds fail at the logon challenge or mid-handshake. ## Importing data @@ -46,6 +46,13 @@ The import tool pulls game data *you* supply: cmangos' public ## Architecture at a glance +A client never speaks to SpacetimeDB. It speaks SRP6 and WoW opcodes, so the gateway is the +translator and every mutation the world can undergo is a reducer call. The realm is sharded across +several databases that all run the *same* wasm. `dev up` brings up that fixture: Eastern Kingdoms on +`lyracore`, Kalimdor on `lyracore-kalimdor`, dungeons on `lyracore-instances`, and accounts and +sessions on `lyracore-realm`. Crossing between World Shards is an escrowed character transfer, not a +walk. `dev up --single` brings up `lyracore` alone. + ``` unmodified 1.12.1 clients (build 5875) │ raw TCP · SRP6 · header-encrypted opcodes diff --git a/docs/architecture.md b/docs/architecture.md index bf475a913..523a93275 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,6 +1,6 @@ # LyraCore architecture -**Status:** current — verified against the tree on 2026-08-04. Every claim below cites the file +**Status:** current — verified against the tree on 2026-09-03. Every claim below cites the file that makes it true; where an older document disagreed with the code, the code won. **Authority note:** [`danger-zones.md`](./danger-zones.md) is authoritative over this document and @@ -68,15 +68,15 @@ routing fact; module game logic never reads one, and an architecture test fails ### 2.1 The module owns -- **Every durable table** — 176 `#[table]` declarations (167 in the core module, plus 9 - contributed by in-tree extension packages), of which 109 are `public` and 67 private. Full - inventory is covered in depth in the maintainers' internal docs; §4 below is the summary. -- **Every state transition** — 240 `#[reducer]` functions in the core module (116 of them in a - default build, the rest behind the `debug_reducers` feature), plus more from any installed - extension package. -- **All periodic work** — 12 scheduled tables drive combat swings, creature AI, aura ticks, ground - areas, instance reaping, transfer reaping, and event GC. Nothing on a gateway timer decides - gameplay. +- **Every durable table** — every `#[table]` declaration lives in `module/src/**`, plus any an + installed extension package contributes. §4 and [`schema.md`](./schema.md) §2 hold the summary; + `grep -rn '^#\[table(' module/src --include='*.rs' | wc -l` is the count that cannot go stale. +- **Every state transition** — every `#[reducer]` function, likewise: + `grep -rn '^#\[reducer' module/src --include='*.rs' | wc -l`. Roughly a third of them are behind + the `debug_reducers` feature and absent from a plain build (§8). +- **All periodic work** — scheduled tables drive combat swings, creature AI, aura ticks, ground + areas, motion republishing, weather, and every reaper. + [`schema.md`](./schema.md) §6 lists them all. Nothing on a gateway timer decides gameplay. - **All authorization of writes** — a reducer derives its actor from `ctx.sender`. ### 2.2 The gateway owns @@ -268,15 +268,17 @@ connectivity. Full inventory and the load-bearing row shapes are covered in depth in the maintainers' internal docs. The summary: -- **176 tables**, `game_`-prefixed for core and `pkg__`-prefixed for packages. External gtker - crates keep their `wow_` names and are never renamed. -- **109 public / 67 private.** `public` means "subscribable by a client connection". Private tables +- **238 tables on 2026-09-03**, `game_`-prefixed for core and `pkg__`-prefixed for packages. + External gtker crates keep their `wow_` names and are never renamed. Recount with + `grep -rn '^#\[table(' module/src --include='*.rs' | wc -l`; the per-domain breakdown is + [`schema.md`](./schema.md) §2. +- **122 public / 116 private.** `public` means "subscribable by a client connection". Private tables (`game_account`, `game_session`, `game_operator`, every region/transfer/instance/realm-core table) are readable only over the owner token. -- **16 `#[client_visibility_filter]` RLS filters**, every one of the same shape - `SELECT * FROM WHERE = :sender`, scoping owner-private rows - (`game_character`, `game_item_instance`, `game_player_spell`, …) and recipient-addressed events - (`game_group_event`, `game_whisper_event`, `game_xp_event`, …). +- **No `#[client_visibility_filter]` RLS filters.** The sixteen owner-scoped filters this document + used to list went out in commit `7fda35a` (2026-08-11), with the per-player client connections + they were the only thing that applied to. §5.4 and [`schema.md`](./schema.md) §4 give the + consequences. - **Spatial partitioning is baked into the rows.** `game_world_entity` carries `grid_x`/`grid_y` cell columns and indexes `(map_id, instance_id, grid_x, grid_y)`; seven event/motion tables carry the identical 4-column grid key so an AOI box query is a range scan. @@ -372,9 +374,10 @@ broadcast tier. operator-gated (`gw_*`, `require_operator`), and the sender-path player reducers are deleted (#483) — but **the model's safety still rests on `:3000` being unreachable**, and nothing in this repository enforces that today. -- **RLS does not gate delivery.** The gateway reads every table through the owner token and gates - visibility with its own predicates (§5.3); the deployed `client_visibility_filter` rules only - ever applied to client-identity subscriptions, which no longer exist. +- **RLS does not gate delivery, and there is no RLS left.** The gateway reads every table through + the owner token and gates visibility with its own predicates (§5.3). The + `client_visibility_filter` rules only ever applied to client-identity subscriptions; those + subscriptions are gone, and commit `7fda35a` (2026-08-11) deleted the rules with them. - **RLS validation is partial, and knowing which half matters.** Preflight's RLS-filter validation step checks every filter against the generated bindings and **fails** if a filter names an unknown table or column — so an identifier typo no longer survives to production. What preflight @@ -538,15 +541,17 @@ table outside a shrinking whitelist; every character-keyed table must carry Each has a companion ratchet test that fails when its whitelist names something that no longer needs to be there. -**Debug reducers are compiled out by default.** `module/Cargo.toml` declares -`debug_reducers = []` with no `default`, and `module/src/debug/` (#386 split the former single -`debug.rs` into a directory along its section banners) is `#![cfg(feature = -"debug_reducers")]` in its entirety. A debug build adds 124 reducers (109 in `debug/`, 15 more -gated individually elsewhere — one `#[cfg(feature = "debug_reducers")]` per function — across ten -other files, including the not-obviously-named `set_guid_floor` in `auth.rs`; `grep -rn -'cfg(feature = "debug_reducers")' module/src` finds every one of them. A production build must be a -plain build; the deploy wrapper `lyracore publish` enables the feature deliberately because the -local headless-client tests need it. +**Debug reducers are compiled out by default.** `module/Cargo.toml` declares `debug_reducers = []` +with no `default`, and `module/src/debug/` is `#![cfg(feature = "debug_reducers")]` in its entirety. +Enabling the feature adds 127 reducers: 112 in `debug/`, and 15 more gated one function at a time +across ten other files, including the not-obviously-named `set_guid_floor` in `auth.rs`. +`grep -rn 'cfg(feature = "debug_reducers")' module/src` finds every one of them. + +What a real deploy actually publishes is not decided here. +[`danger-zones.md`](./danger-zones.md) §3 is authoritative, and today it publishes **with** +`--features=debug_reducers` and runs the unoptimized `./target/debug/lyracore-gateway` binary on +every realm, production included. That is an accepted posture of the alpha, which the README's +warning banner states plainly, not a contradiction to resolve in this document. --- @@ -560,6 +565,7 @@ local headless-client tests need it. | [`../CONTEXT.md`](../CONTEXT.md) | The glossary. The words this document and the code are supposed to use, and the words to avoid. | | [`danger-zones.md`](./danger-zones.md) | **Authoritative.** Traps, tooling gotchas, and the exact deploy/verify procedure. Read before any engine change. | | [`schema.md`](./schema.md) | The table-level data model. | +| [`package-api.md`](./package-api.md) | The Package API, version 1: what a Package's Rust half may call, and what core promises about it. | | [`region-sharding.md`](./region-sharding.md) | Retired (#471): the removed region tier's design — seam menus, assignments, view merge — kept for reference. | ### Operating and building @@ -569,7 +575,24 @@ local headless-client tests need it. | [`quickstart.md`](./quickstart.md) | The shortest path from a clean checkout to a running realm. | | [`development-cli.md`](./development-cli.md) | The `./lyracore` CLI: the pinned shim, and the build, preflight, publish and local-stack commands. | | [`data-ingestion.md`](./data-ingestion.md) | Where vanilla content comes from and the licensing firewall. | -| [`aura-capacity-verification.md`](./aura-capacity-verification.md) | The live-stack procedure proving the 32-buff/16-debuff cap end to end: refusal, untouched survivors, the overflow log line, and the wire-level `SMSG_SPELL_FAILURE` relay. | -| [`mount-verification.md`](./mount-verification.md) | The land-mount fixture ids, the attended procedure, and the Headless Client scenario for the next pinned release. | + +### Verification write-ups + +Each of these records one behaviour proven against a live stack, with the fixture, the exact +procedure, and what the result was. They are reproducible recipes, not status reports. §8 above +explains why two rungs of the ladder are written down instead of automated. + +| Document | What it verifies | +|---|---| +| [`aura-capacity-verification.md`](./aura-capacity-verification.md) | The 32-buff/16-debuff cap end to end: refusal, untouched survivors, the overflow log line, and the wire-level `SMSG_SPELL_FAILURE` relay. | +| [`aura-stacking-probes.md`](./aura-stacking-probes.md) | The stacking-family decision on real `game_aura` rows, as an operator sees it. | +| [`cc-diminishing-returns-probe.md`](./cc-diminishing-returns-probe.md) | Crowd-control diminishing returns, whose persisted state and removal-time window only a live database shows. | +| [`vmap-rollout.md`](./vmap-rollout.md) | Exact collision on both populated World Shards, and that the Instance Pool receives no open-world vmap generation. | +| [`mount-verification.md`](./mount-verification.md) | The land-mount fixture ids, the attended procedure, and the Headless Client scenario. | +| [`taxi-flight-verification.md`](./taxi-flight-verification.md) | The direct-route flight baseline, and the cancel path when catalogue geometry mutates mid-flight. | +| [`movement-batch-acceptance.md`](./movement-batch-acceptance.md) | The steady-heartbeat batching path under a load driver, on a `disposable:` realm only. Script in `scripts/`. | +| [`auction-house-client-check.md`](./auction-house-client-check.md) | The auction house against a real 5875 client. Status: outstanding, needs a human. | +| [`duel-client-check.md`](./duel-client-check.md) | Duel visuals against a real 5875 client, which the automated tests cannot see. Status: outstanding. | +| [`hunter-pet-live-check.md`](./hunter-pet-live-check.md) | Taming, pet bars and pet lifecycle against a live development realm and a real client. | **The work queue is GitHub Issues**, which is the single source of truth for what is open. diff --git a/docs/aura-stacking-probes.md b/docs/aura-stacking-probes.md index e309c91ae..7a3b7b707 100644 --- a/docs/aura-stacking-probes.md +++ b/docs/aura-stacking-probes.md @@ -12,9 +12,9 @@ Run these probes on a development database only. Every step writes gameplay stat - The module must be published with debug reducers. `./lyracore publish` bakes them in; a bare `spacetime publish` does not. Follow [`danger-zones.md`](./danger-zones.md) §3 for the deploy, and never pass `-c`. -- `scripts/publish-module.sh` calls `debug_repair_after_publish` after every publish. That reducer - re-seeds the stacking families and the probe fixture, because `init` does not re-run on an - auto-migrating publish. Call it by hand if you published another way: +- Call `debug_repair_after_publish` by hand after every publish. Nothing runs it for you. That + reducer re-seeds the stacking families and the probe fixture, because `init` does not re-run on an + auto-migrating publish, and without it the probes read as "family missing": ```bash spacetime call -s local lyracore debug_repair_after_publish diff --git a/docs/danger-zones.md b/docs/danger-zones.md index 98f2c0dcd..4ce780794 100644 --- a/docs/danger-zones.md +++ b/docs/danger-zones.md @@ -441,10 +441,12 @@ sleep 4 # RUST_LOG=info deliberately, not info,gateway::world=debug: since the raw-bytes relay, every packet # logs a line, which floods at scale. -# `scripts/publish-module.sh` already calls `debug_repair_after_publish` after every publish above -# (#378) — it re-arms the creature tick + aura/ground-area/instance-reaper schedules and re-seeds -# every fixture family `init` seeds but a plain republish doesn't re-run. No manual call needed here -# unless you bypassed the script. +# ⚠ Now call `debug_repair_after_publish` BY HAND, on EVERY shard you just published. Nothing runs +# it for you. It re-arms the motion, creature-tick, aura, ground-area, weather, gateway-lease and +# instance-reaper schedules and re-seeds every fixture family `init` seeds but an auto-migrating +# republish does not re-run. Skipping it presents as a mid-session hang, not an error. +# Folding it into `lyracore publish` is issue 41 in the lyracore-cli repository. +spacetime call -s local lyracore debug_repair_after_publish # ...and once per further shard # If you changed world DATA (spawns/quests/items): re-run the content import. # After EVERY shard is published or re-imported, prove the replicated catalogues (spells, items, the diff --git a/docs/schema.md b/docs/schema.md index 9219e7abd..e9497550b 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -1,6 +1,6 @@ # Schema — the SpacetimeDB data model -**Status:** current — verified against the tree on 2026-08-04. This document is a *map* of the data +**Status:** current — verified against the tree on 2026-09-03. This document is a *map* of the data model, not a copy of it. **The code is authoritative**: every table is a Rust `#[table]` struct in `module/src/**` — or in an extension package compiled into the same module — and where a snippet here and the code disagree, the code wins. @@ -25,9 +25,9 @@ Start at [`architecture.md`](./architecture.md) for how these tables fit into th single `#[table(…)]` attribute. - **Visibility.** `public` = subscribable by a client connection. Omitting it makes the table **private** — only the database owner (the gateway's coordinator connection) can read it. -- **RLS.** A public table holding per-player rows additionally carries a - `#[client_visibility_filter]`, which requires SpacetimeDB's `unstable` feature (pinned in - `module/Cargo.toml` with the reason). +- **RLS.** The module declares no `#[client_visibility_filter]` today; §4 says why, and what a new + one would still have to satisfy. `module/Cargo.toml` keeps SpacetimeDB's `unstable` feature + pinned, because the attribute needs it. - **Migration discipline.** Every table is designed to grow by **END-appending** a column with an explicitly typed `#[default(...)]`. Ordering-sensitive data uses explicit sequence or timestamp columns, never auto-inc ordering. @@ -54,34 +54,39 @@ publish presents as an unrelated mid-session hang, not a loud "no such table". ## 2. Inventory -**177 tables** — 168 of them in `module/src/**`, the remaining 9 contributed by extension packages -compiled into the same module. **109 public, 68 private.** +**238 tables**, all of them in `module/src/**`: 122 public, 116 private. No table comes from a +package in this tree; `packages/example` is the only in-tree package and it declares none. Recount +rather than trust the numbers below, which drift on every schema change: + +```bash +grep -rn '^#\[table(' module/src --include='*.rs' | wc -l # 238 on 2026-09-03 +``` | Domain | Tables | Public | Where | |---|---:|---:|---| -| Auth / session / identity | 5 | 0 | `auth.rs` | -| Character + per-character progression | 13 | 13 | `character.rs`, `skill.rs`, `reputation.rs`, `talent.rs`, `spell/spellbook.rs`, `action_bar.rs`, `chat.rs`, `combo.rs`, `rest.rs`, `corpse.rs`, `xp.rs`, `exploration.rs` | -| World entity + movement | 8 | 7 | `world.rs`, `creatures/tick.rs` | -| Terrain / nav | 2 | 2 | `terrain.rs`, `nav.rs` | -| Chat / social / addon bridge | 7 | 7 | `chat.rs`, `bridge.rs` | -| Combat / threat | 6 | 3 | `combat/mod.rs`, `threat.rs` | -| Spell / aura | 23 | 11 | `spell/tables.rs`, `spell/spellbook.rs`, `spell/stacking.rs` | -| Talent tree (static) | 2 | 2 | `talent.rs` | -| Quest | 9 | 8 | `quest.rs` | -| Item / vendor | 4 | 3 | `items/tables.rs` | +| Auth / session / identity | 6 | 0 | `auth.rs` | +| Character and per-character progression | 23 | 19 | `character.rs`, `skill.rs`, `reputation.rs`, `talent.rs`, `spell/spellbook.rs`, `action_bar.rs`, `combo.rs`, `rest.rs`, `corpse.rs`, `xp.rs`, `exploration.rs`, `breath.rs`, `breath_relay.rs`, `graveyard.rs` | +| World entity and movement | 8 | 3 | `world.rs`, `motion.rs` | +| Terrain / nav / exact vmap | 9 | 5 | `terrain.rs`, `nav.rs`, `vmap.rs` | +| Chat / social / addon bridge | 9 | 9 | `chat.rs`, `bridge.rs` | +| Combat / threat / duel | 10 | 4 | `combat/engage.rs`, `combat/death.rs`, `threat.rs`, `duel.rs` | +| Spell / aura | 20 | 10 | `spell/tables.rs`, `spell/stacking.rs` | +| Quest | 12 | 8 | `quest.rs` | +| Item / vendor / trade / mail | 11 | 5 | `items/tables.rs`, `trade.rs`, `mail.rs`, `mail_escrow.rs` | | Auction house | 7 | 2 | `auction.rs` | -| Creature (template, spawn, AI, pet, trainer) | 15 | 11 | `creatures/*`, `trainer.rs` | -| GameObject | 6 | 6 | `gameobject.rs` | -| Loot | 9 | 6 | `loot.rs` | +| Creature (template, spawn, AI, pet, trainer) | 42 | 17 | `creatures/*`, `trainer.rs` | +| GameObject | 9 | 6 | `gameobject.rs`, `go_model.rs` | +| Loot | 12 | 6 | `loot/*` | | Group / party | 5 | 3 | `group.rs` | -| Instance / encounter | 7 | 0 | `instance.rs`, `encounter.rs` | -| Region / sharding / transfer / load | 7 | 0 | `region.rs`, `load.rs`, `transfer/` | +| Instance / encounter | 7 | 1 | `instance.rs`, `encounter.rs` | +| Sharding: region, transfer, load | 9 | 0 | `region.rs`, `transfer/mod.rs`, `load.rs` | | Realm-core | 2 | 0 | `realm_core.rs` | -| Config / static data / diagnostics | 23 | 22 | `config.rs`, `gm.rs`, `faction.rs`, `skilldata.rs`, `stats.rs`, `action_bar.rs`, `import_meta.rs`, `debug.rs` | -| Taxi progression / service | 5 | 1 | `taxi.rs` | -| GC | 1 | 0 | `gc.rs` | -| Exact vmap generations | 5 | 0 | `vmap.rs` | -| Extension packages | 9 | 2 | compiled into the module; maintained outside this repository | +| Gateway leases | 3 | 0 | `gw.rs` | +| Taxi | 5 | 1 | `taxi.rs` | +| Weather | 3 | 1 | `weather.rs` | +| Packages | 3 | 1 | `package_config.rs`, `package_import.rs`, `script_binding.rs` | +| Event GC | 1 | 0 | `gc.rs` | +| Config / static data / diagnostics | 22 | 21 | `config.rs`, `gm.rs`, `faction.rs`, `skilldata.rs`, `stats.rs`, `import_meta.rs`, `debug/*` | Two shapes recur and are worth naming: @@ -345,53 +350,32 @@ until the generation is verified again or the vmap data is imported again. ## 4. Row-level security -**16 `#[client_visibility_filter]` filters**, every one of the same shape: +**The module declares no `#[client_visibility_filter]` filters.** Recount at any time: -```sql -SELECT * FROM
WHERE = :sender +```bash +grep -rn 'client_visibility_filter' module/src --include='*.rs' ``` -| Filter on | Identity column | Where | -|---|---|---| -| `game_character` | `owner_identity` | `character.rs:176` | -| `game_player_action` | `owner_identity` | `action_bar.rs:60` | -| `game_addon_message` | `recipient_identity` | `bridge.rs:39` | -| `game_whisper_event` | `recipient_identity` | `chat.rs:318` | -| `game_character_contact` | `owner_identity` | `chat.rs:543` | -| `game_group_event` | `recipient_identity` | `group.rs:297` | -| `game_item_instance` | `owner_identity` | `items/tables.rs:219` | -| `game_character_quest` | `owner_identity` | `quest.rs:304` | -| `game_player_reputation` | `owner_identity` | `reputation.rs:45` | -| `game_player_skill` | `owner_identity` | `skill.rs:312` | -| `game_player_spell` | `owner_identity` | `spell/spellbook.rs:27` | -| `game_resurrect_request` | `target_identity` | `spell/tables.rs:588` | -| `game_character_talent` | `owner_identity` | `talent.rs:137` | -| `game_teleport_event` | `recipient_identity` | `world.rs:500` | -| `game_xp_event` | `recipient_identity` | `xp.rs:38` | -| `game_levelup_event` | `recipient_identity` | `xp.rs:71` | - -**Three limits worth stating plainly.** - -1. **RLS cannot express spatial scoping.** `:sender` is a static identity; a filter cannot join on - the caller's *current* position. Interest management is therefore gateway-managed — see - [`architecture.md`](./architecture.md) §5.2. -2. **Not every public table has a filter.** `game_character_explored` is public with none: it has no - `owner_identity` column, and adding one would be a migration on a live table. Before #483 it was - scoped by a per-player subscription predicate plus a gateway-side self guard; the coordinator now - subscribes it unqualified like every other base table (there is no per-player connection left to - put a predicate on), and the self-scoping happens entirely in-gateway, by the same owner-session - lookup the other self-only relay families use (`module/src/exploration.rs:26–32`). -3. **Offline validation covers identifiers, not shapes.** Step 3 of `lyracore preflight` is an - RLS-filter validation pass: it parses every `Filter::Sql` and checks its tables and - columns against the generated bindings — it **fails the preflight** on an unknown identifier. - (This post-dates the comment at `gateway/src/stdb/subscriptions.rs:3305`, which still says nothing - validates RLS offline; that comment is stale.) What is still unvalidated is the *query shape*: - SpacetimeDB keeps this SQL as raw text during extraction, so a filter the node's RLS engine - rejects fails `subscribe()` and breaks **every login**. A new filter shape is a live-verification - item; a renamed column is caught offline. - -The coordinator connection authenticates as the owner and **bypasses RLS entirely** — by design, it -is the cache the gateway reads through. +Only two comment lines match. This document used to list sixteen owner-scoped filters, all of the +shape `SELECT * FROM
WHERE = :sender`. Commit `7fda35a` (2026-08-11) +deleted every one of them, in the same change that deleted per-player client connections. An RLS +filter only ever applied to a client-identity subscription, and no client-identity subscription is +left: the coordinator connection authenticates as the owner and bypasses RLS by design. + +Visibility is therefore entirely gateway-side. The gateway reads every table through the owner token +and decides what each player sees with its own recipient-keyed lookups, audience predicates and +area-of-interest tracker. See [`architecture.md`](./architecture.md) §5.2, §5.3 and §5.4. + +Two constraints survive the removal and bind any filter added later. + +1. **RLS cannot express spatial scoping.** `:sender` is a static identity, so a filter cannot join on + the caller's *current* position. Interest management stays gateway-managed. +2. **Offline validation covers identifiers, not shapes.** Step 3 of `lyracore preflight` parses every + `Filter::Sql` and checks its tables and columns against the generated bindings, and it fails the + preflight on an unknown identifier. It cannot check the query *shape*: SpacetimeDB keeps this SQL + as raw text during extraction, so a filter the node's RLS engine rejects fails `subscribe()` on + the connection that carries it. A new filter shape is a live-verification item; a renamed column + is caught offline. --- @@ -413,32 +397,57 @@ is the cache the gateway reads through. ## 6. Scheduled tables -Twelve scheduled tables drive every periodic and deferred effect in the game. Nothing on a gateway -timer decides gameplay. +**23 scheduled tables** drive every periodic and deferred effect in the game. Nothing on a gateway +timer decides gameplay. Recount and re-list them with: + +```bash +grep -rn 'scheduled(' module/src --include='*.rs' # 23 tables plus 4 comment lines, 2026-09-03 +``` | Scheduled table | Reducer | Cadence | Where | |---|---|---|---| -| `game_melee_schedule` | `tick_melee` | 100 ms | `combat/mod.rs:1183` | -| `game_creature_move_schedule` | `tick_creatures` | 500 ms (sensing every 8th pass → ~4 s) | `creatures/tick.rs:126` | -| `game_ground_area_schedule` | `tick_ground_areas` | 500 ms | `spell/tables.rs:458` | -| `game_aura_schedule` | `tick_auras` | 1 s | `spell/tables.rs:402` | -| `game_event_reaper_schedule` | `reap_movement_events` | 1 s | `gc.rs:15` | -| `game_transfer_reaper_schedule` | `reap_transfers` | 5 s | `transfer/mod.rs` | -| `game_instance_reaper_schedule` | `reap_instances` | 60 s | `instance.rs:276` | -| `game_pending_cast` | `fire_pending_cast` | one-shot at cast completion | `spell/tables.rs:468` | -| `game_pending_spell_impact` | `fire_spell_impact` | one-shot at projectile landing | `spell/tables.rs:515` | -| `game_ranged_impact_schedule` | `ranged_impact` | one-shot at shot landing | `combat/mod.rs:1196` | -| `game_taxi_flight_schedule` | `advance_taxi_flight` | 250 ms while a passenger is active | `taxi.rs` | -| `game_auction_expiry` | `expire_auction` | one-shot at listing expiry | `auction.rs` | - -The interval rows are inserted by `init` (`module/src/seed.rs:1358–1408`), except the transfer reaper -which `begin_transfer` arms lazily and idempotently. Scheduled reducers self-gate on -`ctx.sender() == ctx.database_identity()` so they cannot be driven externally. - -⚠ Re-arming after a schema change is a real operational step: the schedule re-arms exist because a -republish can leave a schedule row stale. `debug_repair_after_publish` (#378) runs all of them — -creature tick, aura, ground-area, instance reaper — in one call, and `scripts/publish-module.sh` -invokes it automatically after every publish. +| `game_motion_publish_schedule` | `publish_motion` | 50 ms (20 Hz) | `motion.rs:102` | +| `game_melee_schedule` | `tick_melee` | 100 ms | `combat/engage.rs:462` | +| `game_duel_schedule` | `tick_duels` | 250 ms | `duel.rs:81` | +| `game_taxi_flight_schedule` | `advance_taxi_flight` | 250 ms while a passenger is active | `taxi.rs:56` | +| `game_creature_move_schedule` | `tick_creatures` | 500 ms (sensing every 8th pass, so ~4 s) | `creatures/tick/mod.rs:178` | +| `game_ground_area_schedule` | `tick_ground_areas` | 500 ms | `spell/tables.rs:632` | +| `game_aura_schedule` | `tick_auras` | 1 s | `spell/tables.rs:576` | +| `game_breath_schedule` | `tick_breath` | 1 s | `breath.rs:32` | +| `game_event_reaper_schedule` | `reap_movement_events` | 1 s (`EVENT_TTL_MICROS`) | `gc.rs:24` | +| `game_transfer_reaper_schedule` | `reap_transfers` | 5 s, armed lazily by `begin_transfer` | `transfer/mod.rs:250` | +| `game_mail_escrow_reaper_schedule` | `reap_mail_escrows` | 5 s, armed lazily | `mail_escrow.rs:68` | +| `game_pet_care_schedule` | `tick_pet_care` | 7.5 s | `creatures/pet_care.rs:18` | +| `game_gateway_lease_reaper_schedule` | `reap_gateway_leases` | 15 s | `gw.rs:71` | +| `game_instance_reaper_schedule` | `reap_instances` | 60 s | `instance.rs:312` | +| `game_weather_schedule` | `tick_weather` | 10 min | `weather.rs:153` | +| `game_pending_cast` | `fire_pending_cast` | one-shot at cast completion | `spell/tables.rs:643` | +| `game_pending_spell_impact` | `fire_spell_impact` | one-shot at projectile landing | `spell/tables.rs:689` | +| `game_ranged_impact_schedule` | `ranged_impact` | one-shot at shot landing | `combat/engage.rs:474` | +| `game_auction_expiry` | `expire_auction` | one-shot at listing expiry | `auction.rs:162` | +| `game_creature_ai_summon_expiry` | `expire_eventai_summon` | one-shot at summon lifetime end | `creatures/eventai/mobility.rs:17` | +| `game_creature_ai_forced_despawn` | `fire_eventai_forced_despawn` | one-shot at the authored despawn time | `creatures/eventai/mobility.rs:51` | +| `game_creature_ai_relay_continuation` | `resume_relay_run` | one-shot at the authored relay delay | `creatures/eventai/relay.rs:295` | +| `game_creature_ai_relay_arrival` | `resume_relay_arrival` | one-shot on movement completion | `creatures/eventai/relay.rs:309` | + +The interval rows are inserted by `init` (`seed_scheduler_arming`, `module/src/seed.rs`), except the +transfer and mail-escrow reapers, which their own entry points arm lazily and idempotently. +Scheduled reducers self-gate on `ctx.sender() == ctx.database_identity()` so they cannot be driven +externally. + +⚠ Re-arming after a schema change is a real operational step: a republish can leave a schedule row +stale, because `init` does not re-run on an auto-migrating publish. `debug_repair_after_publish` +re-arms the motion, creature-tick, aura, ground-area, weather, gateway-lease and instance-reaper +schedules, and re-seeds every fixture family `init` seeds. It does not repair every scheduled table: +the event reaper and melee schedules are outside this reducer. **Nothing runs it for you.** The +operator calls it by hand on every shard after every publish: + +```bash +spacetime call -s debug_repair_after_publish +``` + +Folding this into `lyracore publish` is tracked as issue 41 in the `lyracore-cli` repository. Until +it lands, a skipped call presents as a mid-session hang rather than a loud error. Packages get a periodic hook without a table of their own: `game_tick_pass!` runs at the end of every `tick_creatures` pass, after all core passes, and is expected to self-quantize for a slower diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 80183720b..566a35413 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -28,7 +28,7 @@ wow_world_messages = { version = "0.3", features = ["vanilla", "sync"] } # doesn't re-export it, but it's already a transitive dep (pinned to match via Cargo.lock). wow_world_base = "0.3" # Pinned exactly to match the module's spacetimedb pin and the `spacetime` CLI version this repo is -# verified against (see module/Cargo.toml and scripts/preflight.sh's CLI-version check). +# verified against (see module/Cargo.toml; `lyracore preflight` check 0 asserts the CLI version). spacetimedb-sdk = "=2.7.1" tokio = { version = "1", features = ["rt-multi-thread", "net", "macros", "io-util", "sync", "time"] } anyhow = "1" @@ -39,9 +39,10 @@ getrandom = "0.3" # distinction that policy turns on. # # NOT a new crate in the tree: libc 0.2.189 already resolves here transitively through tokio (and -# through spacetimedb-sdk), so this promotes an existing, already-vetted dependency to a direct one -# — zero new nodes in the graph, zero new licenses for cargo-deny to consider (MIT OR Apache-2.0, -# both already on deny.toml's allow list). +# through spacetimedb-sdk), so this promotes an existing, already-vetted dependency to a direct one: +# zero new nodes in the graph, and no new license to clear (MIT OR Apache-2.0, the same pair the rest +# of the workspace already carries). There is no `deny.toml` in this repository; the maintainers run +# cargo-deny from their own working repository. libc = "0.2" zeroize = "=1.9.0" log = "0.4" diff --git a/module/Cargo.toml b/module/Cargo.toml index f7dac90e6..159930e8d 100644 --- a/module/Cargo.toml +++ b/module/Cargo.toml @@ -13,20 +13,19 @@ description = "LyraCore SpacetimeDB module: all WoW 1.12 game state and logic." crate-type = ["cdylib", "rlib"] [features] -# Layer 1 client-automation test reducers (module/src/debug.rs). OFF by default so a plain build and -# any production publish compile them out entirely (they set arbitrary players' HP/level/position, so -# they must never exist in prod). The test build / automation harness opts in via: +# Layer 1 client-automation test reducers (`module/src/debug/`). They are off by default, but the +# current deploy procedure deliberately enables them for every publish via: # spacetime publish -s local --build-options='--features=debug_reducers' lyracore -# (`--build-options` is forwarded verbatim to the underlying `cargo build` for the wasm target.) +# `--build-options` is forwarded verbatim to the wasm-target `cargo build`. `danger-zones.md` §3 is +# authoritative on this accepted alpha posture. debug_reducers = [] [dependencies] blake3 = "1" -# `unstable` enables row-level-security visibility filters (client_visibility_filter / Filter), -# which the design uses to pin per-owner tables (character, movement_event) to their owner. -# Pinned exactly (not "2.5") — the SpacetimeDB CLI/SDK/module-crate contract is a single unit (see -# gateway/Cargo.toml's spacetimedb-sdk/-lib pins and scripts/preflight.sh's CLI-version check); a -# minor bump here can silently drift the module ahead of what `spacetime` on PATH can publish. +# `unstable` opts into SpacetimeDB's unstable schema API. +# Pinned exactly (not "2.5"): the SpacetimeDB CLI, SDK and module-crate contract is a single unit +# (see gateway/Cargo.toml's spacetimedb-sdk/-lib pins; `lyracore preflight` check 0 asserts the CLI +# version). A minor bump here can silently drift the module ahead of what `spacetime` can publish. spacetimedb = { version = "=2.7.1", features = ["unstable"] } lyracore-shared.workspace = true # The Package Delta artifact and the claim conflict tracer (src/package_import.rs). Pure and diff --git a/rust-toolchain.toml b/rust-toolchain.toml index dff3a008e..e0fa99ba1 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,18 +1,13 @@ -# Pins the DEVELOPMENT toolchain (what you build/test/fmt with day to day) to a specific version so -# `cargo fmt`/`cargo clippy` output and `spacetime generate`'s wasm build are reproducible across -# machines and CI. This is the SAME version as the workspace's declared MSRV -# (`workspace.package.rust-version = "1.93.0"` in Cargo.toml — see that field's comment for why: the -# original 1.80 MSRV was amended to 1.85 on #231, then #229's CI work found even 1.85 isn't -# buildable — `spacetimedb`/`spacetimedb-sdk` themselves declare `rust-version = "1.93.0"` (true at -# 2.5.0 and re-checked at 2.7.1), so there is no real floor below the toolchain pinned here today). -# rustup transparently -# fetches/switches to this exact toolchain for any cargo command run from this repo, and fails fast -# with an actionable "toolchain '1.93.0' is not installed" if it isn't present, rather than silently -# formatting/building with whatever `rustc` happens to be on PATH. +# Pins the development toolchain (what you build, test and format with) so `cargo fmt`, `cargo +# clippy` and `spacetime generate`'s wasm build are reproducible across machines. It is the same +# version as `workspace.package.rust-version` in Cargo.toml, and that field's comment says why the +# two cannot differ. rustup fetches this exact toolchain for any cargo command run from this repo, +# and fails with "toolchain '1.93.0' is not installed" instead of silently using whatever `rustc` is +# on PATH. [toolchain] channel = "1.93.0" components = ["rustfmt", "clippy"] -# The module compiles to wasm (see module/Cargo.toml); the gateway's `spacetime generate` schema -# extraction (scripts/preflight.sh check 2) also builds it. Listed here so a fresh checkout gets the -# target for free instead of a build failing on a missing target with no hint why. +# The module compiles to wasm (see module/Cargo.toml), and the `spacetime generate` schema extraction +# that `lyracore preflight` runs builds it too. Listed here so a fresh checkout gets the target for +# free instead of failing on a missing target with no hint why. targets = ["wasm32-unknown-unknown"]