From b7e1ffb91dd09c2b9d38ea1cc5ef447434acff95 Mon Sep 17 00:00:00 2001 From: Mathih13 Date: Thu, 3 Sep 2026 19:29:23 +0200 Subject: [PATCH 1/3] chore(module): drop tracker refs from comments and stale dead-code allowances Comments in module/src carried ~830 issue-number references. A reader with no tracker access cannot follow them, and CODING_STANDARDS forbids them. Strip the reference tokens only, keeping the surrounding explanation. Delete five stale `allow(dead_code)` attributes whose callers have since landed, mark the three split/reset cores that only a debug reducer reaches with `cfg_attr(not(feature = "debug_reducers"), allow(dead_code))`, and delete the three DR category constants that no code reads. Add `tripwires::issue_reference_tripwire`, a ratchet that fails when a comment under module/src carries a tracker reference again. Co-Authored-By: Claude Fable 5.1 --- module/src/action_bar.rs | 4 +- module/src/auth.rs | 62 +++++------ module/src/bridge.rs | 2 +- module/src/chat.rs | 48 ++++---- module/src/combat/death.rs | 32 +++--- module/src/combat/engage.rs | 4 +- module/src/combat/folds.rs | 8 +- module/src/combat/mod.rs | 2 +- module/src/combat/swing.rs | 36 +++--- module/src/config.rs | 14 +-- module/src/corpse.rs | 4 +- module/src/creatures/ai.rs | 2 +- module/src/creatures/pet.rs | 2 +- module/src/creatures/spawn.rs | 12 +- module/src/creatures/tick/lifecycle.rs | 28 ++--- module/src/creatures/tick/mod.rs | 14 +-- module/src/debug/fingerprint.rs | 2 +- module/src/debug/instance.rs | 24 ++-- module/src/debug/mod.rs | 34 +++--- module/src/debug/repair.rs | 12 +- module/src/encounter.rs | 4 +- module/src/exploration.rs | 12 +- module/src/gameobject.rs | 34 +++--- module/src/gc.rs | 14 +-- module/src/gm.rs | 2 +- module/src/graveyard.rs | 12 +- module/src/group.rs | 73 ++++++------- module/src/gw.rs | 56 +++++----- module/src/helpers.rs | 53 +++++---- module/src/instance.rs | 104 +++++++++--------- module/src/items/economy.rs | 14 +-- module/src/items/inventory.rs | 40 ++++--- module/src/items/mod.rs | 6 +- module/src/items/ops.rs | 18 +-- module/src/items/rules.rs | 4 +- module/src/items/tables.rs | 4 +- module/src/lib.rs | 28 ++--- module/src/load.rs | 16 +-- module/src/loot/mod.rs | 20 ++-- module/src/loot/rolls.rs | 46 ++++---- module/src/motion.rs | 16 +-- module/src/nav.rs | 4 +- module/src/professions.rs | 4 +- module/src/publish_safety.rs | 8 +- module/src/quest.rs | 6 +- module/src/realm_core.rs | 18 +-- module/src/region.rs | 4 +- module/src/reputation.rs | 6 +- module/src/rest.rs | 4 +- module/src/seed.rs | 62 +++++------ module/src/seed/fixtures.rs | 28 ++--- module/src/skill.rs | 4 +- module/src/spell/cast/resolve.rs | 6 +- module/src/spell/control.rs | 2 +- module/src/spell/effects.rs | 6 +- module/src/spell/math.rs | 2 +- module/src/spell/scheduler.rs | 14 +-- module/src/spell/spellbook.rs | 2 +- module/src/spell/stacking.rs | 14 +-- module/src/spell/tables.rs | 4 +- module/src/spell/taxonomy.rs | 6 +- module/src/spell/tests.rs | 18 +-- module/src/stats.rs | 8 +- module/src/talent.rs | 9 +- module/src/terrain.rs | 4 +- module/src/test_scan.rs | 10 +- module/src/trade.rs | 72 ++++++------ module/src/trainer.rs | 8 +- module/src/transfer/harness.rs | 82 +++++++------- module/src/transfer/mod.rs | 115 +++++++++---------- module/src/transfer/tests.rs | 74 ++++++------- module/src/transfer/transport.rs | 52 ++++----- module/src/tripwires.rs | 146 +++++++++++++++++++------ module/src/vmap.rs | 16 +-- module/src/world.rs | 84 +++++++------- module/src/xp.rs | 4 +- 76 files changed, 952 insertions(+), 876 deletions(-) diff --git a/module/src/action_bar.rs b/module/src/action_bar.rs index da8453c45..2f6b323f4 100644 --- a/module/src/action_bar.rs +++ b/module/src/action_bar.rs @@ -66,7 +66,7 @@ crate::character_owned!(delete, fn sweep_delete_game_player_action(ctx, characte actions.id().delete(r.id); } }); -// CROSS-DATABASE transport (issue #19). `id` is a surrogate `#[auto_inc]` PK — the LOGICAL key is +// CROSS-DATABASE transport. `id` is a surrogate `#[auto_inc]` PK — the LOGICAL key is // (character, button) — so it is re-minted at the destination rather than carried; carrying it would // insert explicit ids without advancing the destination's sequence (danger-zones §2). crate::character_owned!(transfer, fn sweep_transfer_game_player_action(ctx, character_guid, io) { @@ -141,7 +141,7 @@ pub(crate) fn grant_createinfo_actions( } } -/// The action-bar write core, actor-explicit (#479): everything [`set_action_button`] does after +/// The action-bar write core, actor-explicit: everything [`set_action_button`] does after /// resolving WHOSE bar this is. Takes the row — the insert arm stamps the owner's RLS identity /// off it. pub(crate) fn apply_set_action_button( diff --git a/module/src/auth.rs b/module/src/auth.rs index 56cb7edd2..0b1d654fb 100644 --- a/module/src/auth.rs +++ b/module/src/auth.rs @@ -65,7 +65,7 @@ fn current_alpha_test_tools_enrollment(ctx: &ReducerContext) -> bool { ) } -/// Ensure a SHADOW account row exists for `account_id` on THIS database (issue #19). +/// Ensure a SHADOW account row exists for `account_id` on THIS database. /// /// Accounts are realm-scoped: SRP6 lives on the logon tier and writes `game_account` on the realm /// (default) database only. An instance/continent shard therefore has no account row at all — and @@ -79,7 +79,7 @@ fn current_alpha_test_tools_enrollment(ctx: &ReducerContext) -> bool { /// /// Deliberate simplification: the username is synthetic (`#`, which no real account name can /// collide with — account names are uppercased alphanumerics) purely to satisfy the `#[unique]` -/// constraint. Upgrade path: realm-core (#22) owns accounts for every shard and this function is +/// constraint. Upgrade path: realm-core owns accounts for every shard and this function is /// deleted. /// Idempotent — a second call is a no-op, and it NEVER overwrites a real account row. The explicit /// id does not advance the table's `auto_inc` sequence (danger-zones §2), which is safe only @@ -151,7 +151,7 @@ fn operator_claim_action( #[reducer] pub fn claim_operator(ctx: &ReducerContext) -> Result<(), String> { let ops = ctx.db.game_operator(); - // `find(0)`, not `find(&0)` — main's #226 clippy sweep dropped the needless borrow. + // `find(0)`, not `find(&0)` — main's clippy sweep dropped the needless borrow. let claimed_identity = ops.id().find(0).map(|operator| operator.identity); if operator_claim_action(claimed_identity, ctx.sender())? == OperatorClaimAction::Insert { ops.insert(Operator { @@ -397,7 +397,7 @@ pub fn establish_session( // No per-character owner_identity restamp here: it existed so per-owner RLS would admit the // player's own connection, which no longer exists — `apply_player_login` stamps owner rows - // via `restamp_owned_data`, and the gateway predicates are the visibility layer (#483). + // via `restamp_owned_data`, and the gateway predicates are the visibility layer. let now = ctx.timestamp; let expires = now @@ -420,7 +420,7 @@ pub fn establish_session( } // =========================================================================================== -// Realm-safe guid allocation (issue #59) +// Realm-safe guid allocation // =========================================================================================== /// Durable high-water mark for `game_character.guid`. Singleton row (`id` is always 0). Private — @@ -440,7 +440,7 @@ pub fn establish_session( /// so a guid this database has ever handed out — locally or by import — can never be handed out /// here again, even after the character that held it is long deleted. /// -/// `create_character` is unrouted (§ premise check, issue #59): the gateway always calls it +/// `create_character` is unrouted (§ premise check): the gateway always calls it /// against the account's home/default database, never a routed shard, so a per-database mark is /// sufficient — no cross-database plumbing on the creation path itself. #[table(accessor = game_guid_allocator)] @@ -458,7 +458,7 @@ fn ratchet_high_water(current: u64, floor: u64) -> u64 { current.max(floor) } -/// The ORIGINAL per-database scan (issue #59's precursor — see `create_character`'s old comment), +/// The ORIGINAL per-database scan (the precursor — see `create_character`'s old comment), /// generalised over guid iterators so it needs no `ReducerContext` either. Used ONLY to seed a /// `game_guid_allocator` row that has never been touched — an auto-migrated database that already /// has characters seeds from their real max instead of resetting to 0 and re-issuing them. @@ -533,7 +533,7 @@ pub(crate) fn next_character_guid(ctx: &ReducerContext) -> u64 { next } -/// Ratchet the mark up to at least `guid`. Two call sites (issue #59): `transfer::apply_import_blob` +/// Ratchet the mark up to at least `guid`. Two call sites: `transfer::apply_import_blob` /// right after it materialises an imported character (AC#3 — so THIS database can never later /// re-issue a guid it just received from another one), and `world::cascade_delete_character` right /// before it removes a `game_character` row on ANY delete path (defect 1 — so a database that has @@ -553,13 +553,13 @@ pub(crate) fn bump_guid_high_water(ctx: &ReducerContext, guid: u64) { write_high_water(ctx, existing, mark); } -/// **Issue #108 — the guid range THIS database was assigned, and its licence to mint.** +/// **The guid range THIS database was assigned, and its licence to mint.** /// /// `game_character.guid` is a per-database `#[auto_inc]`-style mark, so two databases that both /// MINT characters mint colliding guids — and `transfer_id` **is** the character guid, so a /// collision means two characters sharing an escrow ledger row. It was demonstrated live: guid 59 /// was one character on `lyracore` and a different one on `lyracore-world-1`, and routing -/// sent the second player to the first's shard. #59 already built the receiving half +/// sent the second player to the first's shard. The receiving half already exists /// (`apply_import_blob` and `cascade_delete_character` ratchet the mark), so a shard can never /// re-issue a guid it was *given*; this table is the other half: a shard that mints its own starts /// from a floor nobody else will reach. @@ -569,7 +569,7 @@ pub(crate) fn bump_guid_high_water(ctx: &ReducerContext, guid: u64) { /// below both `2^53` (above which `spacetime call` mangles u64 arguments — danger-zones) and the /// high-bit type markers real entity guids carry. /// -/// #103's first cut (since deleted, issue #390) gave each database its floor by hand through a +/// the first cut (since deleted) gave each database its floor by hand through a /// standalone reducer that only ratcheted `game_guid_allocator.high_water` — a collision became /// *impossible*, but the guarantee rested on an operator remembering to run it on every new shard; /// a shard provisioned without it minted from zero into `lyracore`'s range, silently at creation, @@ -618,12 +618,12 @@ pub(crate) fn require_guid_range(ctx: &ReducerContext) -> Result<(), String> { } /// Is `guid` inside `range`? Pure, same reason as [`may_mint`] — testable without a -/// `ReducerContext`. Issue #237: `transfer::apply_import_blob` calls this to decide whether an +/// `ReducerContext`. `transfer::apply_import_blob` calls this to decide whether an /// ARRIVING character's guid may ratchet THIS database's `game_guid_allocator` — ranges are -/// disjoint by construction (#108), so a guid outside this +/// disjoint by construction, so a guid outside this /// database's own range belongs to another shard and can never collide with anything this shard /// mints; ratcheting past it anyway is pure self-harm (it walks this shard's own mark toward, or -/// past, its own range end for a guid it will never be asked to re-mint — the live #237 +/// past, its own range end for a guid it will never be asked to re-mint — the live /// incident). /// /// `None` (no range installed yet) is conservatively `false`, i.e. "not inside" — never a bump. @@ -639,7 +639,7 @@ pub(crate) fn in_guid_range(range: Option<(u64, u64)>, guid: u64) -> bool { guid >= base && guid < base.saturating_add(size) } -/// Install the range realm-core assigned to this database (#108). Idempotent — re-installing the +/// Install the range realm-core assigned to this database. Idempotent — re-installing the /// same range is how the gateway confirms one is held — and it never *moves* a range: a different /// base is an error, not an overwrite. /// @@ -690,12 +690,12 @@ pub fn install_guid_range(ctx: &ReducerContext, base: u64) -> Result<(), String> mod guid_allocator_tests { use super::{allocate_next_guid, legacy_guid_seed, ratchet_high_water}; - /// **Issue #103.** Two shards that both mint characters must never mint the same guid, and the + /// Two shards that both mint characters must never mint the same guid, and the /// mechanism is a per-shard floor. The property that makes a floor SAFE to apply to a live /// database — and safe to re-apply — is that it can only ratchet UP: a floor below the current /// mark must not rewind the allocator onto guids that are already issued. `install_guid_range` - /// (below) is the only caller left that applies one (issue #390 deleted the standalone - /// `set_guid_floor` reducer — dead since #108 gave every shard's floor a licensed, non-optional + /// (below) is the only caller left that applies one (deleted the standalone + /// `set_guid_floor` reducer — dead once every shard's floor got a licensed, non-optional /// path instead), and it reaches the mark through this same function, so this pin still covers /// it. Mutation target: swapping `ratchet_high_water(seed, floor)` for a plain assignment inside /// `bump_guid_high_water` makes the second assertion re-issue guid 40. @@ -713,7 +713,7 @@ mod guid_allocator_tests { ); } - /// **Issue #108.** The floor above is monotonic but *optional* — a shard nobody ran it on mints + /// The floor above is monotonic but *optional* — a shard nobody ran it on mints /// from zero. This is the rule that makes it non-optional, and the two refusals are the whole /// point: no range at all, and a mark that has escaped the range it was assigned. #[test] @@ -736,7 +736,7 @@ mod guid_allocator_tests { ); } - /// **Issue #237.** `in_guid_range` is the pure decision `transfer::apply_import_blob` calls to + /// `in_guid_range` is the pure decision `transfer::apply_import_blob` calls to /// decide whether an ARRIVING character's guid may ratchet this database's allocator. /// Boundary-checked the same way as `may_mint` just above (base included, end excluded), and, /// like it, conservative when there is no range installed at all. @@ -768,7 +768,7 @@ mod guid_allocator_tests { /// …and the WIRING (playbook §8 — the repo's dominant test failure is pinning the helper and /// not the call site). `create_character` must consult the rule BEFORE it writes anything; - /// deleting that one line brings back exactly the silent minting #108 exists to end, with every + /// deleting that one line brings back exactly the silent minting this rule exists to end, with every /// other test in this file still green. #[test] fn create_character_refuses_before_it_writes_anything() { @@ -809,7 +809,7 @@ mod guid_allocator_tests { } /// Mutation target: swap the `.max` between the two iterators, or drop either arm, and this - /// goes red — it's the exact scan `create_character` used to run inline (issue #59). + /// goes red — it's the exact scan `create_character` used to run inline. #[test] fn legacy_guid_seed_is_the_max_of_both_sources() { assert_eq!( @@ -838,10 +838,10 @@ mod guid_allocator_tests { ); } - /// Issue #59 defect 1, COMPOSED end-to-end through the pure functions (no ctx, so this can run + /// Defect 1, COMPOSED end-to-end through the pure functions (no ctx, so this can run /// as a plain unit test): a database with characters up to 49 has JUST cascade-deleted guid 50 /// (a departed transfer) — `legacy_guid_seed` therefore only sees the survivors, exactly the - /// scan `create_character` used to run inline before issue #59. Flooring the mark at 50 (what + /// scan `create_character` used to run inline. Flooring the mark at 50 (what /// `cascade_delete_character`'s new `bump_guid_high_water` call does) must make the NEXT /// allocation land on 51, never re-issue the departed 50. This is the outcome the individual /// `ratchet_high_water`/`allocate_next_guid` tests above don't quite prove on their own — this @@ -858,7 +858,7 @@ mod guid_allocator_tests { } /// `body_of`/`code_of`/`shape_of` are the shared scan primitives in [`crate::test_scan`] - /// (issue #64 — this used to be six near-identical copies, drifted apart). + /// (this used to be six near-identical copies, drifted apart). use crate::test_scan::shape_of; /// Collapse whitespace the same way [`shape_of`] does, for a hand-written "what it SHOULD be" @@ -901,7 +901,7 @@ mod guid_allocator_tests { } /// Same reasoning as above, for the other entry point: `bump_guid_high_water` must actually - /// ratchet at `guid` and persist the result, or issue #59's defect 1/AC#3 fix (the two call + /// ratchet at `guid` and persist the result, or the defect 1/AC#3 fix (the two call /// sites in `world::cascade_delete_character` and `transfer::apply_import_blob`) does nothing. #[test] fn bump_guid_high_water_is_exactly_the_pinned_shape() { @@ -959,7 +959,7 @@ pub fn create_character( facial_hair: u8, ) -> Result<(), String> { crate::helpers::require_operator(ctx)?; - // #108: minting is licensed by a range realm-core assigned, and an unlicensed database refuses + // Minting is licensed by a range realm-core assigned, and an unlicensed database refuses // rather than minting from zero into another shard's guids. FIRST — before any write, and // before the cheaper validations, so the refusal is unambiguous in the log. require_guid_range(ctx)?; @@ -971,7 +971,7 @@ pub fn create_character( // Per-realm character cap (vanilla: 10). The 5875 client can't render a char list with >10 entries // (it rejects SMSG_CHAR_ENUM → "Error retrieving character list"), so refuse the 11th here with a // distinguished error the gateway maps to CHAR_CREATE_SERVER_LIMIT ("cannot create any more"). - // Routed through the `by_account` index (issue #390) — this used to be a full-table scan. + // Routed through the `by_account` index — this used to be a full-table scan. if chars.by_account().filter(&account_id).count() >= 10 { return Err("SERVER_LIMIT".to_string()); } @@ -1004,7 +1004,7 @@ pub fn create_character( .or_else(|| positions.race_class().find(human_warrior)) .ok_or_else(|| "no start position seeded".to_string())?; - // Realm-safe GUID allocation (issue #59): a durable high-water mark, not a per-transaction scan + // Realm-safe GUID allocation: a durable high-water mark, not a per-transaction scan // of THIS database alone. The old scan (still the seed a never-touched mark bootstraps from — // see `legacy_guid_seed`) looked free the instant a cross-database transfer deleted the departed // character's row (and its items) here, and the next creation could re-issue that guid while the @@ -1114,14 +1114,14 @@ pub fn delete_character( if c.account_id != account_id { return Err("NOT_OWNER".to_string()); } - // IN-TRANSIT FENCE (issue #16). `cascade_delete_character` runs the `character_owned!` delete + // IN-TRANSIT FENCE. `cascade_delete_character` runs the `character_owned!` delete // sweeps, which include `sweep_delete_game_transfer_out` — so a delete landing mid-transfer wipes // the character AND both escrow rows in one transaction, voiding a transfer another shard is // still driving. Cross-database that is the annihilation case: the destination's in-row and its // arrival copy survive the source's delete, and its reaper (no out-row ⇒ `Hold`) never settles // them. The escrow must be resolved (finished or reaped) before the character can be destroyed. // - // Routed through the shared gate (issue #30) rather than a bespoke `is_in_transit` call, but the + // Routed through the shared gate rather than a bespoke `is_in_transit` call, but the // DISTINGUISHED error is kept: the raw `find` above is what lets NO_SUCH_CHAR / NOT_OWNER / // CHAR_IN_TRANSIT stay three different answers, which `character_by_guid` alone would flatten to // NO_SUCH_CHAR. The gateway maps every non-OK delete the same way, so no seam either way. diff --git a/module/src/bridge.rs b/module/src/bridge.rs index 50ed40fcc..a94bac24e 100644 --- a/module/src/bridge.rs +++ b/module/src/bridge.rs @@ -51,7 +51,7 @@ pub(crate) fn send(ctx: &ReducerContext, character_guid: u64, cmd: &str, payload }); } -/// The shared core behind [`client_command`] and its gateway twin `gw_client_command` (#479): +/// The shared core behind [`client_command`] and its gateway twin `gw_client_command`: /// dispatch with the sender already resolved to a guid. pub(crate) fn apply_client_command( ctx: &ReducerContext, diff --git a/module/src/chat.rs b/module/src/chat.rs index 761e91ce7..5ba3c8a1a 100644 --- a/module/src/chat.rs +++ b/module/src/chat.rs @@ -63,7 +63,7 @@ pub fn normalized_message(raw: &str) -> Option { Some(trimmed.chars().take(MAX_CHAT_LEN).collect()) } -/// The say/yell core, actor-explicit (#468 stage 4a): everything the old sender-path `send_chat` +/// The say/yell core, actor-explicit (stage 4a): everything the old sender-path `send_chat` /// did after resolving WHO spoke. Trims + length-caps the message and rejects an empty one; an /// unsupported chat type is a clean `Err` (the gateway drops it). `gw::gw_send_chat` delegates here. pub(crate) fn apply_send_chat( @@ -148,7 +148,7 @@ pub fn normalize_channel(name: &str) -> String { /// Join `channel` (065, CMSG_JOIN_CHANNEL): dedupe on (key, character). The gateway acks with /// SMSG_CHANNEL_NOTIFY(YouJoined) unconditionally — vanilla treats a re-join as idempotent. /// -/// The channel-join core, actor-explicit (#479) — same split as [`apply_send_chat`]. +/// The channel-join core, actor-explicit — same split as [`apply_send_chat`]. pub(crate) fn apply_join_channel( ctx: &ReducerContext, sender: crate::WorldEntity, @@ -179,7 +179,7 @@ pub(crate) fn apply_join_channel( /// Leave `channel` (065, CMSG_LEAVE_CHANNEL). Idempotent — leaving a channel you're not in is a no-op. /// -/// The channel-leave core, actor-explicit (#479) — same split as [`apply_send_chat`]. +/// The channel-leave core, actor-explicit — same split as [`apply_send_chat`]. pub(crate) fn apply_leave_channel( ctx: &ReducerContext, sender: crate::WorldEntity, @@ -203,7 +203,7 @@ pub(crate) fn apply_leave_channel( /// client can't normally send to an un-joined channel — a modified one gets an Err). Same /// dead-guard + length cap as say/yell. /// -/// The channel-speak core, actor-explicit (#479) — same split as [`apply_send_chat`]. +/// The channel-speak core, actor-explicit — same split as [`apply_send_chat`]. pub(crate) fn apply_send_channel_message( ctx: &ReducerContext, sender: crate::WorldEntity, @@ -276,7 +276,7 @@ pub struct EmoteEvent { /// `text_emote` / `emote_anim` ids come from the client; invalid ones degrade gracefully gateway-side /// (the text line is skipped / the animation is dropped) rather than erroring. /// -/// The text-emote core, actor-explicit (#468 stage 4a) — same split as [`apply_send_chat`]. +/// The text-emote core, actor-explicit (stage 4a) — same split as [`apply_send_chat`]. pub(crate) fn apply_send_emote( ctx: &ReducerContext, sender: crate::WorldEntity, @@ -329,8 +329,8 @@ pub struct WhisperEvent { pub is_inform: bool, pub message: String, pub created_at: Timestamp, - /// The recipient's CHARACTER GUID (issue #22, whisper slice). END-appended + defaulted, so this - /// is an additive auto-migration and every pre-#22 row reads back as 0. + /// The recipient's CHARACTER GUID (whisper slice). END-appended + defaulted, so this + /// is an additive auto-migration and every earlier row reads back as 0. /// /// `recipient_identity` cannot address a recipient on REALM-CORE: an identity is minted per /// (account, database) by the node, so the identity a player holds on a world shard names nobody @@ -407,14 +407,14 @@ fn prepare_system_message( /// a clean `Err` the gateway turns into `SMSG_CHAT_PLAYER_NOT_FOUND`. Name match is case-insensitive /// (vanilla `/w bob` reaches "Bob"). /// -/// **The SHARD plane** of #22's whisper slice: this core resolves the target inside the CALLING +/// **The SHARD plane** of the whisper slice: this core resolves the target inside the CALLING /// database, which is exactly why a whisper could not cross a shard boundary, and it stays the only -/// path a single-database gateway ever takes (byte-identical to pre-#22 — same gates, same order, +/// path a single-database gateway ever takes (byte-identical to earlier — same gates, same order, /// same rows). A multi-database gateway routes to [`realm_whisper`] instead; the ROW SHAPE both /// planes write is the one shared core below ([`whisper_rows`] + [`push_whisper`]), so the ignore /// rule and the sender's echo cannot drift between them. /// -/// The shard-plane whisper core, actor-explicit (#479) — everything the old sender-path +/// The shard-plane whisper core, actor-explicit — everything the old sender-path /// `send_whisper` did after resolving WHO spoke. Same split as [`apply_send_chat`]; /// `gw::gw_send_whisper` is the entry. pub(crate) fn apply_send_whisper( @@ -424,7 +424,7 @@ pub(crate) fn apply_send_whisper( message: String, ) -> Result<(), String> { let text = normalized_message(&message).ok_or_else(|| "empty message".to_string())?; - // Case-insensitive name match (vanilla `/w bob` reaches "Bob"). REFUSE verdict (issue #30): the + // Case-insensitive name match (vanilla `/w bob` reaches "Bob"). REFUSE verdict: the // fenced `character_by_name` reads an in-transit character as ABSENT, so a whisper aimed at a // character mid-shard-hop falls into the existing not-found arm the gateway already maps to // SMSG_CHAT_PLAYER_NOT_FOUND — no new error string, no gateway edit. @@ -452,7 +452,7 @@ pub(crate) fn apply_send_whisper( Ok(()) } -/// The REALM-CORE plane of the whisper (issue #22, whisper slice): deliver `message` from +/// The REALM-CORE plane of the whisper (whisper slice): deliver `message` from /// `sender_guid` to `target_guid`, both named by GUID because a name means nothing here. /// /// **Operator-gated, and it has to be** — the same trust boundary `realm_group_op` sits on. It takes @@ -495,13 +495,13 @@ pub fn realm_whisper( } /// The rows one whisper produces, as `(recipient_guid, other_guid, is_inform)` — the whole delivery -/// rule of a whisper, extracted so it is the SAME on the shard plane and on realm-core (issue #22). +/// rule of a whisper, extracted so it is the SAME on the shard plane and on realm-core. /// /// Two rows, because a whisper is two chat lines: the incoming line to the target /// (" whispers: …") and the echo to the sender ("To : …"). The echo is unconditional /// and the incoming line is not: an ignored sender's line is dropped at the source, so no /// `SMSG_MESSAGECHAT` can reach the ignorer, and the sender is told nothing (vanilla shows no error — -/// the whisper simply never arrives). Order is incoming-then-echo, matching pre-#22 insert order. +/// the whisper simply never arrives). Order is incoming-then-echo, matching earlier insert order. /// /// Pure — unit-tested without a `ReducerContext`, which is the only way either plane's delivery rule /// is testable in this crate at all. @@ -518,7 +518,7 @@ pub(crate) fn whisper_rows( rows } -/// Insert one whisper row, addressed BOTH ways (issue #22, whisper slice): by `recipient_guid` (the +/// Insert one whisper row, addressed BOTH ways (whisper slice): by `recipient_guid` (the /// realm-wide name, which the gateway's realm-core relay self-filters on) and by the recipient's bound /// `recipient_identity` (the per-player RLS a world shard delivers through, unchanged). /// @@ -576,7 +576,7 @@ fn push_whisper( /// Unlike `send_chat`, this core takes no `language` argument: like whispers, party lines aren't /// language-filtered (always Universal on the wire), so there is nothing to thread through. /// -/// The party-chat core, actor-explicit (#479) — same split as [`apply_send_chat`]. +/// The party-chat core, actor-explicit — same split as [`apply_send_chat`]. pub(crate) fn apply_party_chat( ctx: &ReducerContext, sender: crate::WorldEntity, @@ -663,7 +663,7 @@ crate::character_owned!(delete, fn sweep_delete_game_character_contact(ctx, char contacts.id().delete(r.id); } }); -// CROSS-DATABASE transport (issue #19): the character's OWN friend/ignore rows travel with it (`id` +// CROSS-DATABASE transport: the character's OWN friend/ignore rows travel with it (`id` // re-minted — surrogate PK). Rows where it is the TARGET belong to OTHER characters and stay where // their owner is, which is why only `by_owner` is exported: a friend list is per-owner state, and // copying someone else's row would fork it. @@ -741,7 +741,7 @@ pub(crate) fn remove_contact( // `CMSG_ADD_FRIEND` / `CMSG_DEL_FRIEND` / `CMSG_ADD_IGNORE` / `CMSG_DEL_IGNORE` all land on // `add_contact`/`remove_contact` above via `gw::gw_add_friend`/`gw_del_friend`/`gw_add_ignore`/ -// `gw_del_ignore` (#483 — the sender-path wrappers are gone). +// `gw_del_ignore` (the sender-path wrappers are gone). // =========================================================================================== // Random roll [event] — `/roll` broadcast (MSG_RANDOM_ROLL) @@ -782,7 +782,7 @@ pub struct RollEvent { /// (`min > max`) by swapping and caps both ends at 10 000 (the vanilla client ceiling). /// Dead players can roll (vanilla allows it). /// -/// The `/roll` core, actor-explicit (#479) — same split as [`apply_send_chat`]. +/// The `/roll` core, actor-explicit — same split as [`apply_send_chat`]. pub(crate) fn apply_send_roll( ctx: &ReducerContext, roller: crate::WorldEntity, @@ -948,7 +948,7 @@ mod tests { assert_eq!(party_chat_other_recipients(7, &[7]), Vec::::new()); } - // ---- Whisper delivery (issue #22, whisper slice) ---- + // ---- Whisper delivery (whisper slice) ---- /// The delivery rule both planes share. A whisper is TWO lines, and which of them exists is the /// whole of the ignore rule: the echo is unconditional (the sender is never told they were @@ -956,7 +956,7 @@ mod tests { /// relay can reach the ignorer. #[test] fn a_whisper_writes_the_incoming_line_and_the_senders_echo() { - // (recipient, other, is_inform) — incoming first, echo second, matching pre-#22 order. + // (recipient, other, is_inform) — incoming first, echo second, matching earlier order. assert_eq!( whisper_rows(10, 20, false), vec![(20, 10, false), (10, 20, true)] @@ -988,7 +988,7 @@ mod tests { ); } - // ---- `realm_whisper`'s two unreachable decisions (issue #22, whisper slice) ---- + // ---- `realm_whisper`'s two unreachable decisions (whisper slice) ---- // // A reducer body needs a live `ReducerContext`, so the two below cannot be EXECUTED by a test in // this crate — which is why they are scanned. Same technique, and the same reason, as @@ -996,7 +996,7 @@ mod tests { /// The `//`-stripped body of `signature`'s function — assert on CODE, never on the prose beside /// it. Shared with every other file's copy of this scan as [`crate::test_scan::code_of`] - /// (issue #64 — this used to be six near-identical, drifted-apart copies). + /// (this used to be six near-identical, drifted-apart copies). use crate::test_scan::code_of; /// **The operator gate is the entire authorization of the realm-core whisper plane.** @@ -1103,7 +1103,7 @@ mod tests { "whisper_rows(sender_guid, target_guid, sender_is_ignored)", ), ( - // #479 factored the shard plane's body out of the `send_whisper` reducer into the + // Factored the shard plane's body out of the `send_whisper` reducer into the // actor-explicit core both entries (sender + `gw_send_whisper`) delegate to — the // pin follows the body, which is where the delivery rule actually lives. "the SHARD plane", diff --git a/module/src/combat/death.rs b/module/src/combat/death.rs index 40d09c47c..54f06f0a1 100644 --- a/module/src/combat/death.rs +++ b/module/src/combat/death.rs @@ -1,5 +1,5 @@ -//! Death + the shared damage pipeline (#382 split of the former monolithic `combat/mod.rs`, on top of -//! #370's shared damage pipeline). `kill_creature`/`kill_player` are the two chokepoints every lethal +//! Death + the shared damage pipeline (split of the former monolithic `combat/mod.rs`, on top of +//! the shared damage pipeline). `kill_creature`/`kill_player` are the two chokepoints every lethal //! path funnels through so a melee kill, `debug_set_health(0)`, and a lethal DoT tick all produce an //! IDENTICAL corpse/release. `fold_incoming_damage` (the MODIFIER stage: outgoing % → incoming % → //! absorb → godmode), `final_damage` (the EventAI lethal floor), and `apply_hit` (the APPLICATION @@ -577,7 +577,7 @@ fn kill_creature_with_attribution( roll_corpse_loot(ctx, &mut target, target_guid, entitlement.as_ref()); target.health = 0; target.dead = true; - // #519: a creature killed mid-leg (flee/patrol/chase) still carries an in-flight + // A creature killed mid-leg (flee/patrol/chase) still carries an in-flight // `game_creature_spline` row with a real duration — left alone, the client keeps interpolating the // corpse toward the old destination after death (a Kobold Vermin sliding onward while dead). Same // "TOLD to halt instead of left interpolating" 0-duration stop the cycle's chase phase uses when a @@ -594,13 +594,13 @@ fn kill_creature_with_attribution( // (`world_view.rs`), so a bare delete relays nothing — the corpse would keep sliding exactly as // before this fix. // 2. `game_creature_spline` deletion is deliberately confined to ONE chokepoint, - // `despawn_creature_entity` (issue #359's "canonical despawn checklist" — see its doc + tripwire + // `despawn_creature_entity` (the "canonical despawn checklist" — see its doc + tripwire // in `tick/lifecycle.rs`), which forbids new deletion sites for this table on the same grounds - // #395 retired the old per-caller copies. Adding a second deletion path here would be exactly - // the divergence #359 exists to prevent. + // Retired the old per-caller copies. Adding a second deletion path here would be exactly + // the divergence this checklist exists to prevent. // So the row is left for the existing 60s corpse-decay reap (`pass_decay` → `despawn_creature_entity`, // a LATER transaction) to clear, same as it always has for every other creature death. This means - // #519's literal "no row for the dead guid" Done-when only becomes true after that reap, not + // the literal "no row for the dead guid" Done-when only becomes true after that reap, not // immediately on kill — the stop-spline packet and position are what a headless check right after // kill can assert; row absence is a 60s-later assertion. See the issue-519 comment reconciling this. if ctx @@ -780,10 +780,10 @@ fn award_tag_rewards( /// Roll a dead creature's money + item loot onto its corpse, apply the GROUP loot method's /// need/greed/round-robin/master-loot stamping, and mark it LOOTABLE if anything dropped. Also purges -/// any stale corpse-loot residue on this guid (issue #358) BEFORE rolling fresh drops — the fix MUST +/// any stale corpse-loot residue on this guid BEFORE rolling fresh drops — the fix MUST /// run on every kill so a leftover pickpocket row can't collide with a freshly-rolled kill-drop slot /// (see `corpse_residue_tripwire` below, which pins the ordering). Extracted out of `kill_creature`'s -/// inline body (issue #382) so the death sequence reads as a table of contents. +/// inline body so the death sequence reads as a table of contents. fn roll_corpse_loot( ctx: &ReducerContext, target: &mut WorldEntity, @@ -878,7 +878,7 @@ pub(crate) fn kill_player(ctx: &ReducerContext, victim_guid: u64, killer_guid: u // who dies is dismounted HERE, by ordinary aura removal — the corpse must not keep its mount, its // buffs, or the crowd control that was on it. crate::spell::remove_auras_on_death(ctx, victim_guid); - // A live Trade Session dies with the victim — both windows hear `TradeCanceled` (#123). + // A live Trade Session dies with the victim — both windows hear `TradeCanceled`. crate::trade::cancel_trade_for(ctx, victim_guid); crate::items::apply_death_durability_loss(ctx, victim_guid); crate::hooks::fire_on_death( @@ -894,11 +894,11 @@ pub(crate) fn kill_player(ctx: &ReducerContext, victim_guid: u64, killer_guid: u } // =========================================================================================== -// The SHARED damage pipeline (#370) [entity] +// The SHARED damage pipeline [entity] // // Everything that happens to a target AFTER the damage number is rolled used to exist in four // near-verbatim copies — the main-hand swing, the off-hand swing, the ranged projectile impact, and -// `spell::apply_target_damage`. They drifted twice (issue #361: the off-hand ignored Disarm, the +// `spell::apply_target_damage`. They drifted twice (the off-hand ignored Disarm, the // ranged impact ignored godmode), which is what a copy of a pipeline always eventually does. The // pipeline now lives here, exactly once, in three stages: // @@ -1087,8 +1087,8 @@ pub(crate) fn fold_incoming_damage( /// /// A 0-damage hit (miss, fully absorbed, godmode) is a complete no-op: no health write, no kill, no /// break-on-damage, no threat — matching what the melee swing and `apply_target_damage` have always -/// done, and what the ranged impact now does too (before #370 it ran the survivor path with a 0 -/// damage value on a godmode target — the same drift class #361 fixed twice). +/// done, and what the ranged impact now does too (before it ran the survivor path with a 0 +/// damage value on a godmode target — the same drift class fixed twice). /// /// The one 0-damage hit that is NOT inert is a hit a Lethal Damage Floor reduced to nothing: it /// still refreshes the pursuit leash and fires the death-prevented hook once, because the creature @@ -1425,13 +1425,13 @@ mod lethality_tests { mod corpse_residue_tripwire { use crate::test_scan::code_of; - /// Issue #358: `roll_pickpocket_loot` inserts `game_corpse_loot` rows at slots 0.. on the LIVE + /// `roll_pickpocket_loot` inserts `game_corpse_loot` rows at slots 0.. on the LIVE /// creature's guid; if the mob dies before every row is taken, `roll_creature_loot` re-inserts /// kill drops starting at slot 0 on the SAME guid, producing duplicate `(corpse_guid, slot)` pairs /// that every first-match loot consumer addresses ambiguously. The fix is a purge that MUST run /// on every kill, BEFORE the fresh roll — this is `ReducerContext` glue invisible to a behavioural /// test (no in-process DB harness here), so the wiring is pinned directly, in two parts since - /// issue #382 extracted the corpse/loot step out of `kill_creature`'s inline body: + /// Extracted the corpse/loot step out of `kill_creature`'s inline body: /// `kill_creature` must still route every kill through `roll_corpse_loot`, and `roll_corpse_loot` /// itself must call `loot::purge_corpse_residue` strictly before `roll_creature_loot`. Losing any /// of the three pieces silently reopens the collision. diff --git a/module/src/combat/engage.rs b/module/src/combat/engage.rs index beea7f862..8886e3028 100644 --- a/module/src/combat/engage.rs +++ b/module/src/combat/engage.rs @@ -1,4 +1,4 @@ -//! The engagement model (#382 split of the former monolithic `combat/mod.rs`, on top of #370's shared +//! The engagement model (split of the former monolithic `combat/mod.rs`, on top of the shared //! damage pipeline): `enter_combat`/`disengage`/`clear_target`/`break_own_attacks` + the `is_engaged`/ //! `melee_combatant_guids` queries over `game_melee_attack` (the single source of truth for who fights //! whom — see the banner below for the both-directions rule those two helpers encode), the engagement @@ -494,7 +494,7 @@ pub struct RangedImpactSchedule { // =========================================================================================== /// The TARGET-side half of the attack-command gate, shared verbatim by `apply_start_attack` and -/// `apply_start_ranged_attack` (#370 — it was a copy-paste, and a copy of a gate is a gate that +/// `apply_start_ranged_attack` (it was a copy-paste, and a copy of a gate is a gate that /// eventually only half-holds): the target must EXIST, be on the same map + instance, not be a /// CORPSE, and not be FRIENDLY. Returns the resolved target row so the caller doesn't fetch it twice. /// diff --git a/module/src/combat/folds.rs b/module/src/combat/folds.rs index f644438e5..abc84d859 100644 --- a/module/src/combat/folds.rs +++ b/module/src/combat/folds.rs @@ -1,5 +1,5 @@ -//! Effective-value folds over aura/gear + the weapon-damage/attack-table roll family (#382 split of the -//! former monolithic `combat/mod.rs`, on top of #370's shared damage pipeline). Everything here is the +//! Effective-value folds over aura/gear + the weapon-damage/attack-table roll family (split of the +//! former monolithic `combat/mod.rs`, on top of the shared damage pipeline). Everything here is the //! "what does this unit's swing look like right now" layer: the `effective_*` stat folds (dodge/parry/ //! block/crit/miss/swing-time/armor/strength/agility/move-speed), the seal-on-swing + queued-strike //! (Heroic Strike/Cleave) on-next-swing folds, the react-window (Overpower/Revenge) arm/check, the @@ -336,8 +336,8 @@ fn mounted_move_pct(move_pct: i32, mounted_pct: i32, mounted: bool) -> i32 { /// (a shield/holdable frill in the off-hand, a relic/quiver in the ranged slot), or it holds a BROKEN /// weapon (has durability and it hit 0 — that unit swings unarmed until it is repaired). The ONE /// equipped-weapon reader: main-hand / off-hand / ranged all differ only in the slot they name, and -/// keeping three copies of this join is exactly how the "non-weapon" and "broken" gates drift apart -/// (issue #370). The tooltip `damage_min`/`damage_max` are `f32`; rounded to whole points for the +/// keeping three copies of this join is exactly how the "non-weapon" and "broken" gates drift apart. +/// The tooltip `damage_min`/`damage_max` are `f32`; rounded to whole points for the /// integer swing math (vanilla weapon damage is whole numbers). `subclass` is the weapon subclass /// (BOW=2, GUN=3, CROSSBOW=18, WAND=19, …) — only the ranged path reads it. [entity] fn weapon_profile_in_slot( diff --git a/module/src/combat/mod.rs b/module/src/combat/mod.rs index db3fa41c6..6b1c916fa 100644 --- a/module/src/combat/mod.rs +++ b/module/src/combat/mod.rs @@ -6,7 +6,7 @@ //! a **player** target also dies at 0 HP (health=0 + `dead` → the death/release/ghost corpse-run, see //! `world::repop`). [entity]/[event]/[server] //! -//! Submodules (#382 split along this file's own comment banners, on top of #370's shared damage +//! Submodules (split along this file's own comment banners, on top of the shared damage //! pipeline): //! - `tables` — the ctx-free hit-table/damage/regen math (pure formulas, exhaustive tests; unchanged //! by the split — it is the model file for the module). diff --git a/module/src/combat/swing.rs b/module/src/combat/swing.rs index 6c7cd13e4..34fd19ac8 100644 --- a/module/src/combat/swing.rs +++ b/module/src/combat/swing.rs @@ -1,4 +1,4 @@ -//! The swing tick (#382 split of the former monolithic `combat/mod.rs`, on top of #370's shared damage +//! The swing tick (split of the former monolithic `combat/mod.rs`, on top of the shared damage //! pipeline): `tick_melee`'s three passes (`leash_pass`/`aggro_pass`/`resolve_swing`), the positional //! gate (`swing_blocked`), and the resolvers that actually roll + fire a hit (`fire_melee_swing`, //! `resolve_offhand_swing`, `fire_ranged_shot`, and the scheduled `ranged_impact` reducer). Every @@ -192,7 +192,7 @@ fn aggro_pass(ctx: &ReducerContext) { /// persists its own `last_offhand_swing_ms` stamp directly — it does not rely on the main hand's /// tail-of-loop stamp, which that path's various early returns (corpse/CC/lethal) may skip. /// -/// The DAMAGE half is not duplicated at all any more (#370): the same +/// The DAMAGE half is not duplicated at all any more: the same /// `fold_incoming_damage` → `final_damage` → event → [`apply_hit`] the main-hand swing and ranged /// impact use, over the off-hand's AP-scaled range reduced by `apply_offhand_penalty` (vanilla's 50% /// dual-wield penalty). What stays off-hand-specific is only what vanilla makes MAIN-HAND-only: it does not arm @@ -251,9 +251,9 @@ fn resolve_offhand_swing( crate::spell::break_stealth(ctx, attacker_guid); - // The SHARED pipeline (#370): outgoing % → incoming % → absorb → godmode, then the wire event, + // The SHARED pipeline: outgoing % → incoming % → absorb → godmode, then the wire event, // then the application (rage/skill/lethal fork/health/break-on-damage/threat). Identical to the - // main-hand swing by construction — this is exactly the copy whose Disarm gate drifted (#361). + // main-hand swing by construction — this is exactly the copy whose Disarm gate drifted. let (dmg, _absorbed) = fold_incoming_damage(ctx, attacker_guid, target_guid, rolled); let damage = final_damage(ctx, target_guid, dmg); let lethal = is_lethal(target.health, damage.amount); @@ -294,7 +294,7 @@ fn resolve_offhand_swing( /// Pass 2 — swings. For each engagement (re-read, since aggro may have added rows), run the SHARED /// ELIGIBILITY GATE and, if it passes, fire the swing. /// -/// This function is the gate and nothing else (#370): both parties still in the world and not +/// This function is the gate and nothing else: both parties still in the world and not /// corpses, the attacker not crowd-controlled and not routing, the ranged weapon still equipped, the /// vanilla movement rule for a ranged loop, [`swing_blocked`]'s positional checks, the off-hand's /// independent second stream, and the swing timer. What a swing then DOES lives in one of two @@ -421,7 +421,7 @@ fn resolve_swing(ctx: &ReducerContext) { } // POSITIONAL eligibility — max range, ranged minimum range, line of sight, facing — all four - // in ONE gate (#370). They used to be four separate `if ... { if ranged_due { delete } continue }` + // in ONE gate. They used to be four separate `if ... { if ranged_due { delete } continue }` // blocks, i.e. four copies of the teardown rule ("a blocker on a DUE ranged shot INTERRUPTS the // auto-repeat loop; a melee blocker just waits for the next tick") — one per gate, each free to // drift. `swing_blocked` is side-effect-free and short-circuits in the same order, so this is @@ -489,7 +489,7 @@ fn resolve_swing(ctx: &ReducerContext) { // FIRE. Everything above is the SHARED eligibility gate — existence, corpse, CC, rout, the // ranged weapon, the movement rule, position, the off-hand's independent stream, and the swing - // timer. What a fired swing then DOES splits cleanly in two (#370): a melee swing (the full + // timer. What a fired swing then DOES splits cleanly in two: a melee swing (the full // attack table, the seal / next-swing / react procs, an instant hit) and a ranged shot (a // reduced table, ammo, and a projectile whose damage lands on a scheduled impact). Each // resolver owns its own wire event and its own `last_swing_ms` stamp, and both apply damage @@ -696,7 +696,7 @@ fn fire_melee_swing( wear_weapon(ctx, attacker_guid); } - // The SHARED modifier fold (#370): outgoing % → incoming % → absorb → godmode. `lethal` is needed + // The SHARED modifier fold: outgoing % → incoming % → absorb → godmode. `lethal` is needed // HERE, before the events, for the wire's `killing_blow` flag; `apply_hit` re-derives it for the // fork from the same shared predicate. let (dmg, _absorbed) = fold_incoming_damage(ctx, attacker_guid, target_guid, rolled); @@ -757,7 +757,7 @@ fn fire_melee_swing( }); } - // The SHARED application (#370): rage both ways, weapon/defense skill-ups, the lethal fork through + // The SHARED application: rage both ways, weapon/defense skill-ups, the lethal fork through // kill_player/kill_creature, the health write, break-on-damage, and threat. let combat_ended = apply_hit( ctx, @@ -889,7 +889,7 @@ fn fire_ranged_shot( // melee swing. crate::spell::break_stealth(ctx, attacker_guid); - // The SHARED modifier fold (#370). Vanilla folds absorb at impact; this engine freezes that + // The SHARED modifier fold. Vanilla folds absorb at impact; this engine freezes that // chain at launch, then resolves the lethal floor and damage log together at impact. let (dmg, _absorbed) = fold_incoming_damage(ctx, attacker_guid, target_guid, rolled); @@ -966,11 +966,11 @@ pub fn ranged_impact(ctx: &ReducerContext, shot: RangedImpactSchedule) { // The rest of the chain (outgoing %, damage-taken %, absorb) was folded and FROZEN at launch by // `fire_ranged_shot`, so the delayed damage LOG equals what actually lands — but godmode can be // toggled on DURING the arrow's flight, and re-checking only the frozen value would let a delayed - // arrow land damage that a melee swing at the same instant would zero (issue #361). `false` for a + // arrow land damage that a melee swing at the same instant would zero. `false` for a // non-godmode target, so this is byte-identical for them. let dmg = if target.godmode { 0 } else { shot.damage }; let damage = final_damage(ctx, shot.target_guid, dmg); - // The SHARED pipeline (#370) — the same one the main-hand and off-hand swings route through, so a + // The SHARED pipeline — the same one the main-hand and off-hand swings route through, so a // shot can never again drift from a swing (a godmode-zeroed hit is now a full no-op here too, // instead of running the survivor path with a 0 damage value). let outcome = apply_hit( @@ -998,14 +998,14 @@ pub fn ranged_impact(ctx: &ReducerContext, shot: RangedImpactSchedule) { #[cfg(test)] mod damage_pipeline_drift_tests { - // #361 found TWO live divergences in what was then a copy-pasted post-roll damage pipeline — + // Found TWO live divergences in what was then a copy-pasted post-roll damage pipeline — // off-hand swings ignored Disarm, ranged impacts ignored godmode — and pinned each fix in place. - // #370 removed the copies: there is now ONE pipeline (`fold_incoming_damage` → `final_damage` + // Removed the copies: there is now ONE pipeline (`fold_incoming_damage` → `final_damage` // → `apply_hit`) that // every damaging path routes through, so the drift class those two tests guard against can no // longer be introduced one resolver at a time. These tests therefore moved UP a level: instead of // pinning each fix in each copy, they pin that every resolver still goes through the one - // chokepoint (plus the two #361 guards that remain genuinely resolver-local). + // chokepoint (plus the two guards that remain genuinely resolver-local). // // There is no `ReducerContext` harness in this crate by design (see `test_scan`'s doc comment / // playbook §7), so — same as the other chokepoint tests in this file — these pin the wiring's @@ -1149,7 +1149,7 @@ mod damage_pipeline_drift_tests { #[test] fn offhand_swing_checks_disarm_before_rolling_its_range() { - // #361, still resolver-local: Disarm strips the MAIN hand inside `swing_range_ctx`, and the + // Still resolver-local: Disarm strips the MAIN hand inside `swing_range_ctx`, and the // off-hand derives its own range, so the gate has to be read here too. let body = code_of(include_str!("swing.rs"), "fn resolve_offhand_swing("); assert!( @@ -1162,7 +1162,7 @@ mod damage_pipeline_drift_tests { #[test] fn ranged_impact_re_checks_godmode_at_impact_time() { - // #361, still resolver-local: the rest of the modifier chain is frozen at LAUNCH, so this is + // Still resolver-local: the rest of the modifier chain is frozen at LAUNCH, so this is // the one fold the impact must re-evaluate (godmode can be toggled during the arrow's flight). let body = code_of(include_str!("swing.rs"), "pub fn ranged_impact("); assert!( @@ -1175,7 +1175,7 @@ mod damage_pipeline_drift_tests { #[test] fn the_ranged_teardown_rule_is_written_once() { - // #370: the "a blocker on a DUE ranged shot INTERRUPTS the auto-repeat loop" teardown used to + // The "a blocker on a DUE ranged shot INTERRUPTS the auto-repeat loop" teardown used to // be repeated at each of the four positional gates (range / minimum range / LoS / facing). // Those gates now live in the side-effect-free `swing_blocked`, so `resolve_swing` states the // teardown once. Pinned by absence: the gates' own predicates must not reappear inline. diff --git a/module/src/config.rs b/module/src/config.rs index 325729600..45ce65f4a 100644 --- a/module/src/config.rs +++ b/module/src/config.rs @@ -59,16 +59,16 @@ pub struct ServerConfig { // or `UPDATE game_config SET nav_enabled = false WHERE id = 0`. #[default(true)] pub nav_enabled: bool, - // END-APPENDED (issue #39): does THIS database host dungeon-instance POPULATIONS? Default `true` + // END-APPENDED: does THIS database host dungeon-instance POPULATIONS? Default `true` // = every single-database realm behaves exactly as it always has (the portal spawns the dungeon // where the player is standing). Set `false` on the OPEN-WORLD shard of a multi-database - // deployment (spec #12 Phase A): `create_instance_with_id` then files the `game_instance` row + + // deployment (spec Phase A): `create_instance_with_id` then files the `game_instance` row + // binding as a LEASE and spawns nothing, so the world writer never pays for a dungeon whose run // happens on another database — the instances shard, where this stays `true`, spawns the // population when the gateway mirrors the id there via `ensure_instance`. // - // Deliberately a WORLD POLICY, not a shard id: the module still knows nothing about shards (spec - // #12), it only knows whether it hosts instance populations — the `nav_enabled` precedent. + // Deliberately a WORLD POLICY, not a shard id: the module still knows nothing about shards; + // it only knows whether it hosts instance populations — the `nav_enabled` precedent. // Operator-set, like `nav_enabled`: `UPDATE game_config SET hosts_instances = false WHERE id = 0`. // Deliberate simplification: one SQL line in the Phase A runbook rather than a gateway→module // policy push. Ceiling: an operator who forgets it gets today's behavior (the dungeon spawns @@ -84,15 +84,15 @@ pub struct ServerConfig { // Operator-set like `nav_enabled`: `UPDATE game_config SET bots_idle = true WHERE id = 0`. #[default(false)] pub bots_idle: bool, - // END-APPENDED (issue #521, decision #10): exact per-cell vmap collision-triangle consumption - // gate — the LoS/collision ray queries in `vmap::los_ray`/`vmap::collision_ray`, and (#523) + // END-APPENDED (decision): exact per-cell vmap collision-triangle consumption + // gate — the LoS/collision ray queries in `vmap::los_ray`/`vmap::collision_ray`, and // `nav::has_los`'s consumers (aggro/assist/creature-casts/engage/swing-gate/caster hold-range) // plus Blink's collision clamp. Default OFF: the standard import pipeline // (`importer/scripts/import-world.sh`) has no vmap step, so a normally-provisioned world has // ZERO `game_vmap_chunk` rows for every map — and the missing-chunk contract (no row = "no // obstruction known here") then reads as "every ray is clear" MAP-WIDE, not just per-cell. // That's fine for a partially-covered map (the intended degrade) but wrong as a global - // default while vmap import is a manual, unwired path (#520/#521). Flip per-map only after + // default while vmap import is a manual, unwired path. Flip per-map only after // `importer --vmap` has actually populated `game_vmap_chunk` for it. Toggle: // `debug_set_vmap_enabled` or `UPDATE game_config SET vmap_enabled = true WHERE id = 0`. #[default(false)] diff --git a/module/src/corpse.rs b/module/src/corpse.rs index cd7199384..d8960da75 100644 --- a/module/src/corpse.rs +++ b/module/src/corpse.rs @@ -172,7 +172,7 @@ pub struct Corpse { pub instance_id: u64, } -/// The corpse-reclaim core, actor-explicit (#479): everything [`reclaim_corpse`] does after +/// The corpse-reclaim core, actor-explicit: everything [`reclaim_corpse`] does after /// resolving WHOSE ghost is reclaiming. Takes the row because the reclaim mutates it in place. pub(crate) fn apply_reclaim_corpse( ctx: &ReducerContext, @@ -252,7 +252,7 @@ pub(crate) fn apply_reclaim_corpse( /// delete (→ SMSG_DESTROY_OBJECT), same as reclaim. /// /// Called from `gc.rs`'s `reap_movement_events` tick — the same sibling-sweep pattern as -/// `spell::stacking::sweep_dr_state` / `loot::sweep_loot_rolls` (#379 pulled the inline block out of +/// `spell::stacking::sweep_dr_state` / `loot::sweep_loot_rolls` (this pulled the inline block out of /// `gc.rs`, which had no business knowing what bones are). /// /// Full scan is safe: the corpse table holds at most one row per RECENTLY-dead player (reclaim diff --git a/module/src/creatures/ai.rs b/module/src/creatures/ai.rs index f8d8df6e7..8bf2f0974 100644 --- a/module/src/creatures/ai.rs +++ b/module/src/creatures/ai.rs @@ -608,7 +608,7 @@ impl TickScope { /// item forbids. They cover ALL instances (including dedicated-row ones) from the catch-all row, /// at the unchanged global cadence — so a dedicated row is a movement/AI smoothing knob only, and /// deleting it can never strand an instance's timers. COROLLARY: the catch-all row is - /// load-bearing — never delete it (`debug_repair_after_publish`, #378, restores it). + /// load-bearing — never delete it (`debug_repair_after_publish` restores it). pub(crate) fn runs_global_passes(&self) -> bool { matches!(self, TickScope::CatchAll { .. }) } diff --git a/module/src/creatures/pet.rs b/module/src/creatures/pet.rs index a464723f2..173113a03 100644 --- a/module/src/creatures/pet.rs +++ b/module/src/creatures/pet.rs @@ -57,7 +57,7 @@ pub struct PetCommand { crate::character_owned!(delete, fn sweep_delete_game_pet_command(ctx, character_guid) { ctx.db.game_pet_command().owner_guid().delete(character_guid); }); -// CROSS-DATABASE transport (issue #19): the row is EPHEMERAL bar state for a live pet ENTITY, and +// CROSS-DATABASE transport: the row is EPHEMERAL bar state for a live pet ENTITY, and // the pet entity does not cross the boundary (it is a `game_world_entity`, not character-owned) — // it despawns with the owner and is re-summoned on the far side, which re-derives the default // Follow+Defensive the absent row already means. Not transported, by decision. diff --git a/module/src/creatures/spawn.rs b/module/src/creatures/spawn.rs index 5f34f614e..7e139a7c7 100644 --- a/module/src/creatures/spawn.rs +++ b/module/src/creatures/spawn.rs @@ -459,9 +459,9 @@ pub fn debug_normalize_spawn_timers(ctx: &ReducerContext, limit: u32) -> Result< Ok(()) } -/// #194: retire every creature whose SPAWN point sits inside the given cell box on THIS database — +/// Retire every creature whose SPAWN point sits inside the given cell box on THIS database — /// spawn row, live entity, spline leg, and (globally, like `import_creature_spawns`) the threat -/// table. Regions are single-owner (creatures are region-static, spec #12): when a region is +/// table. Regions are single-owner (creatures are region-static, spec): when a region is /// assigned to another shard, the non-owner runs this so exactly one database holds the /// population. Keyed by SPAWN position, not the entity's current one — a wanderer belongs to the /// region of its home. Recoverable only by re-import; operator- and debug-feature-gated on purpose. @@ -659,7 +659,7 @@ pub fn scale_creature_damage_for_level( /// enters the world without any core dispatch-site edits. Player-entity inserts (login, /// debug_spawn_player_entity) deliberately do NOT come through here. pub(crate) fn insert_creature_entity(ctx: &spacetimedb::ReducerContext, mut entity: WorldEntity) { - // #526: stand on a model floor (bridge, WMO interior deck) instead of the imported spawn.z + // Stand on a model floor (bridge, WMO interior deck) instead of the imported spawn.z // when one's imported at/below this spawn point — `floor_z` is `None` off vmap-slice/gate, so // an unimported map spawns byte-identical to before this line existed. if let Some(floor) = crate::vmap::floor_z(ctx, entity.map_id, entity.x, entity.y, entity.z) { @@ -794,7 +794,7 @@ pub fn build_creature_entity( /// (the player counterpart to `build_creature_entity`). Resolves the power type, the gender-correct /// per-race display + faction from the imported ChrRaces data (falling back to the Human-Male values /// — display 49 / faction 1 — when the table isn't loaded), and the full stat curve via -/// `stats::apply_level_stats` (the same writer the ding loop and a GM level-set use, #362); health == +/// `stats::apply_level_stats` (the same writer the ding loop and a GM level-set use); health == /// max_health on a fresh build, mana classes start full while rage/energy start empty /// (`stats::starting_power`). /// @@ -889,7 +889,7 @@ pub fn build_player_entity( } else { 0 }, - // Sheath state UNARMED (#101). Deliberately NOT persisted across logout: vanilla rebuilds a + // Sheath state UNARMED. Deliberately NOT persisted across logout: vanilla rebuilds a // player with weapons stowed, and the client re-sends `CMSG_SETSHEATHED` when the player // draws again, so there is nothing to restore. unit_bytes_2: 0, @@ -965,7 +965,7 @@ pub fn build_player_entity( } // The level-derived stat block — the five base attributes, armor, and max health/power — from the // real class/level curve (importer P3), via the ONE shared writer also used by the ding loop and a - // GM level-set (#362). Falls back to the flat placeholder/zeros when the curve isn't loaded, so an + // GM level-set. Falls back to the flat placeholder/zeros when the curve isn't loaded, so an // L1 character is 60 HP either way. crate::stats::apply_level_stats(ctx, &mut entity, character.race, character.class, level); // Resume at the persisted vitals rather than always healing to full. diff --git a/module/src/creatures/tick/lifecycle.rs b/module/src/creatures/tick/lifecycle.rs index dbbd8f848..00f9c8bb7 100644 --- a/module/src/creatures/tick/lifecycle.rs +++ b/module/src/creatures/tick/lifecycle.rs @@ -1,5 +1,5 @@ -//! The creature lifecycle passes (issue #383 split of tick.rs): the canonical despawn checklist -//! (issue #359 — every teardown path in the crate routes through `despawn_creature_entity`), then +//! The creature lifecycle passes (split of tick.rs): the canonical despawn checklist +//! (every teardown path in the crate routes through `despawn_creature_entity`), then //! decay/respawn/GO-respawn — the due-time passes that run on schedule regardless of proximity. See //! `tick/mod.rs`'s module doc for the pipeline's load-bearing pass ORDER; this split does not change //! it (every pass here keeps its original name and signature, called by bare name via `tick/mod.rs`'s @@ -12,7 +12,7 @@ use crate::{game_corpse_loot, game_gameobject, game_gameobject_pool_member, game use super::*; // =========================================================================================== -// The canonical creature-despawn checklist (issue #359) +// The canonical creature-despawn checklist // =========================================================================================== /// Delete a live creature entity AND every satellite row keyed on its guid. **This is the one @@ -22,8 +22,8 @@ use super::*; /// `game_creature_spawn` row and the encounter tracking/equip rows for the first two, the respawn-timer /// re-arm for the third — and none of them re-implements the list below. /// -/// Issue #359: those three had each grown a private copy and DIVERGED. `despawn_tracked` deleted -/// `game_corpse_loot` by corpse guid with no `!withheld` filter — the exact delete the #50 fix removed +/// Those three had each grown a private copy and DIVERGED. `despawn_tracked` deleted +/// `game_corpse_loot` by corpse guid with no `!withheld` filter — the exact delete the fix removed /// from the other two — so a mid-roll wipe on a sharded realm silently ate the winner's item (the roll /// is authoritative on realm-core and invisible here; `settle_loot_roll`'s grant then finds no row and /// no-ops). It also skipped the rest of the loot family and the taunt lock. Adding a satellite table @@ -31,7 +31,7 @@ use super::*; /// /// NOT on the list: `game_creature_move_event`. Two of the three copies scanned that whole table for /// the mover's pending legs; nothing has written it since perf 2.3 moved legs onto the per-creature -/// `game_creature_spline` row (#395 retired the last writers, and the tripwire below forbids new +/// `game_creature_spline` row (retired the last writers, and the tripwire below forbids new /// ones — `gc.rs` stopped reaping it on the same grounds). A full-table scan per despawned creature, /// to delete from a provably empty table, teaches the next reader that the table is live. /// @@ -51,7 +51,7 @@ pub(crate) fn despawn_creature_entity(ctx: &ReducerContext, guid: u64) { let _ = crate::creatures::set_creature_gossip_menu(ctx, guid, None); crate::loot::tag::clear(ctx, guid); ctx.db.game_creature_spline().guid().delete(guid); // the LIVE leg row (perf 2.3) - crate::motion::drop_pending(ctx, guid); // #461: the staged, not-yet-republished payload too + crate::motion::drop_pending(ctx, guid); // The staged, not-yet-republished payload too ctx.db.game_entity_motion().guid().delete(guid); // motion row dies with the entity (perf 2.1) crate::loot::reap_corpse_loot_family(ctx, guid); // item rows (withheld-safe) + eligibility + rolls + votes ctx.db.game_world_entity().guid().delete(guid); // last — the on_delete relay destroys the object @@ -106,7 +106,7 @@ pub(crate) fn pass_decay(ctx: &ReducerContext) -> usize { // future constant change could put deadlines past decay), keep the corpse one more tick // rather than discard a live roll — the sweep resolves it next pass. // - // Issue #50 fix: `sweep_loot_rolls` and the `game_loot_roll` check above only see rolls + // Fix: `sweep_loot_rolls` and the `game_loot_roll` check above only see rolls // whose STATE lives on THIS database. In a sharded deployment a roll promoted to realm-core // clears its local staging row within ~200ms of kill-time, so neither of the above can see // it anymore even though it is still open there — decay would otherwise reap this corpse's @@ -130,7 +130,7 @@ pub(crate) fn pass_decay(ctx: &ReducerContext) -> usize { { continue; } - // The ONE checklist (issue #359): entity + spline/motion + the whole loot family (slice 4's + // The ONE checklist: entity + spline/motion + the whole loot family (slice 4's // item rows and work-item 187's eligibility snapshot + resolved rolls + votes, so a decayed // corpse never orphans them — an UNRESOLVED roll blocked the decay above). This pass's own // extra is the respawn-timer re-arm below. @@ -297,7 +297,7 @@ mod due_timer_tripwire { /// the past and the row is revisited forever. That failure is invisible: behaviour stays /// correct and only the cost comes back. /// - /// Issue #383: moved from `tick.rs`'s combined `due_timer_tripwire` mod to `lifecycle.rs` with + /// Moved from `tick.rs`'s combined `due_timer_tripwire` mod to `lifecycle.rs` with /// the passes it pins (`pass_decay`/`pass_respawn` now live here, not in `tick/mod.rs`). #[test] fn the_due_time_passes_range_scan_their_index_and_disarm_after_firing() { @@ -356,7 +356,7 @@ mod due_timer_tripwire { mod despawn_checklist_tripwire { use crate::test_scan::code_of; - /// **One despawn checklist** (issue #359). `despawn_creature_entity` is the only place that knows + /// **One despawn checklist**. `despawn_creature_entity` is the only place that knows /// what a dying creature leaves behind; pinning its steps is how a satellite table stops being /// dropped from the list by a refactor that "only touched one caller". /// @@ -435,7 +435,7 @@ mod despawn_checklist_tripwire { /// **The three teardown paths route through the checklist and re-implement none of it.** /// - /// This is the actual issue-#359 regression: `encounter::despawn_tracked` carried a private copy + /// This is the actual regression: `encounter::despawn_tracked` carried a private copy /// that deleted `game_corpse_loot` by corpse guid with NO `!withheld` filter, so a wipe mid /// NEED/GREED roll deleted the item out from under a roll that — on a sharded realm — is /// authoritative on realm-core and invisible locally, and `settle_loot_roll`'s grant then found @@ -494,8 +494,8 @@ mod despawn_checklist_tripwire { ); } - /// The #50 invariant lives in ONE function now (`loot::reap_corpse_loot_family`, moved there from - /// `instance.rs` by issue #359 to sit next to the tables it cleans). Losing the `!withheld` + /// The invariant lives in ONE function now (`loot::reap_corpse_loot_family`, moved there from + /// `instance.rs` to sit next to the tables it cleans). Losing the `!withheld` /// filter silently breaks the cross-shard NEED/GREED grant on every teardown path at once. #[test] fn the_canonical_loot_family_reap_never_deletes_a_withheld_row() { diff --git a/module/src/creatures/tick/mod.rs b/module/src/creatures/tick/mod.rs index 4968a01c1..a2114d74e 100644 --- a/module/src/creatures/tick/mod.rs +++ b/module/src/creatures/tick/mod.rs @@ -9,7 +9,7 @@ //! active-cell sweep and rows-visited evidence logs, the shared candidate gate //! `movable_creature`, the rout predicates, and the one spline writer //! (`emit_move_spline`/`emit_creature_leg`) every movement decision funnels through. -//! - [`lifecycle`] — the canonical despawn checklist (issue #359) + decay/respawn/GO-respawn, the +//! - [`lifecycle`] — the canonical despawn checklist + decay/respawn/GO-respawn, the //! due-time passes that run regardless of proximity. use lyracore_shared::spatial; @@ -81,7 +81,7 @@ pub struct CreatureMoveEvent { accessor = game_creature_spline, public, index(accessor = by_grid, btree(columns = [map_id, instance_id, grid_x, grid_y])), - // #456: the AOI cell index — exactly 3 columns, all matched by equality terms, which is the + // The AOI cell index — exactly 3 columns, all matched by equality terms, which is the // only shape SpacetimeDB 2.7.1's subscription planner can serve (see the `cell` column). index(accessor = by_cell, btree(columns = [map_id, instance_id, cell])) )] @@ -117,7 +117,7 @@ pub struct CreatureSpline { pub spline_id: u32, #[default(false)] pub run: bool, - /// #456: `(grid_x, grid_y)` packed into ONE indexed value — the AOI subscription's cell key. + /// `(grid_x, grid_y)` packed into ONE indexed value — the AOI subscription's cell key. /// /// SpacetimeDB 2.7.1's subscription planner can only serve a query from an index when EVERY /// column of that index is matched by an equality term, and it skips any index with more than 3 @@ -139,7 +139,7 @@ pub struct CreatureSpline { /// `backfill_cell_ids` covers it for completeness. #[default(0i64)] pub cell: i64, - /// #518: this leg is a FACING-ONLY packet (the mover does NOT move — `sx/sy/sz` == `dx/dy/dz`, + /// This leg is a FACING-ONLY packet (the mover does NOT move — `sx/sy/sz` == `dx/dy/dz`, /// `dur_ms` is 0) and `facing_angle` is the new heading the client should snap to /// (`SMSG_MONSTER_MOVE`'s `FacingAngle` variant). A stationary stand-and-swing creature never /// throws a normal leg (nothing to interpolate), so without this the client never learns its @@ -577,7 +577,7 @@ pub(crate) fn emit_move_spline( } } -/// #518: write a FACING-ONLY spline row — `guid` doesn't move (start == dest, `dur_ms` 0) but its +/// Write a FACING-ONLY spline row — `guid` doesn't move (start == dest, `dur_ms` 0) but its /// heading changes to `angle_rad`. Routes through the SAME `game_creature_spline` relay carrier as /// every other creature leg (one AOI-scoped table, one gateway subscription) rather than a new one; /// the gateway distinguishes it by the `facing` flag and emits `SMSG_MONSTER_MOVE`'s `FacingAngle` @@ -766,7 +766,7 @@ mod relay_tripwire { /// scan: no module or package source may INSERT into that table. (Reads/reaps are fine — the /// table still exists and `gc.rs` sweeps whatever is left.) /// - /// #357: this used to be a HAND-PICKED file list (`tick.rs` + `encounter.rs` + `spell/cast.rs` + + /// This used to be a HAND-PICKED file list (`tick.rs` + `encounter.rs` + `spell/cast.rs` + /// playerbots), and the list omitted `creatures/pet.rs` — the Follow leg it writes leaked /// undelivered rows into this table every sense tick, unbounded, on a live shard, and the scan /// never saw it. Scan the WHOLE compiled tree instead — `character_owned_tripwire::scanned_files` @@ -778,7 +778,7 @@ mod relay_tripwire { /// at RUN time instead so it is never spelled out contiguously in source anywhere, which is /// self-exclusion that can't rot. /// - /// Issue #383: this test used to live in `tick.rs`'s `due_timer_tripwire` mod alongside the + /// This test used to live in `tick.rs`'s `due_timer_tripwire` mod alongside the /// decay/respawn and aggro tripwires below. Split with the file it pins: `emit_move_spline` /// (the ONE writer this test protects) stays in `tick/mod.rs`, so the test stays here too — the /// other two moved to `lifecycle.rs`/`sense.rs` with the passes they actually test. diff --git a/module/src/debug/fingerprint.rs b/module/src/debug/fingerprint.rs index d006f941d..68da8ceca 100644 --- a/module/src/debug/fingerprint.rs +++ b/module/src/debug/fingerprint.rs @@ -1,4 +1,4 @@ -//! Catalogue parity fingerprint (issue #82) — content-hash the tables that are supposed to be +//! Catalogue parity fingerprint — content-hash the tables that are supposed to be //! IDENTICAL on every shard (spells, items, DBC-derived reference tables), so a partial or stale //! re-import shows up as a loud mismatch instead of "this spell behaves differently in Durotar". //! Row counts alone are not enough (a count matches trivially while contents differ): every row is diff --git a/module/src/debug/instance.rs b/module/src/debug/instance.rs index 47a0fac5f..c9ff5b5e5 100644 --- a/module/src/debug/instance.rs +++ b/module/src/debug/instance.rs @@ -194,7 +194,7 @@ pub fn debug_nav_leg(ctx: &ReducerContext, map: u32, x0: f32, y0: f32, x1: f32, ); } -/// Toggle vmap ray consumption (`game_config.vmap_enabled`, issue #521) — upserts row 0 like +/// Toggle vmap ray consumption (`game_config.vmap_enabled`) — upserts row 0 like /// `debug_set_nav_enabled`. OFF = every ray query returns clear (no vmap data consulted). #[reducer] pub fn debug_set_vmap_enabled(ctx: &ReducerContext, enabled: bool) -> Result<(), String> { @@ -247,7 +247,7 @@ pub fn debug_set_nav_coverage_enabled(ctx: &ReducerContext, enabled: bool) -> Re /// 521 spot-probe: cast an exact vmap ray from (x0,y0,z0) to (x1,y1,z1) on `map`, BOTH flavors — /// LoS (WMO-only) and collision (WMO + M2 doodads). Analogue of `debug_nav_leg`; vmap has no -/// client-visible readback, so this is the server-side verification hook (decision #10). Logs +/// client-visible readback, so this is the server-side verification hook (decision). Logs /// hit/miss + the first-hit point for each flavor so a doodad-vs-wall distinction is directly /// observable: a doodad blocks `collision` but leaves `los` clear. #[reducer] @@ -287,7 +287,7 @@ pub fn debug_floor_probe(ctx: &ReducerContext, map: u32, x: f32, y: f32, probe_z /// 527 spot-probe: log the WMO area-info at `(x, y, z)` for `map` — which group (if any) contains /// the point and whether it's indoor. Analogue of `debug_vmap_ray`/`debug_floor_probe`; vmap has -/// no client-visible readback, so this is the server-side verification hook (decision #10 §query +/// no client-visible readback, so this is the server-side verification hook (decision §query /// surface). Done-when's live check: a point inside the Northshire abbey logs `indoor=true` with a /// group id, a point outside logs `indoor=None`/no group. #[reducer] @@ -301,7 +301,7 @@ pub fn debug_vmap_area_info(ctx: &ReducerContext, map: u32, x: f32, y: f32, z: f ); } -/// #526 done-when: server-side proof that a probe (and a creature spawn) stand on a model floor +/// Done-when: server-side proof that a probe (and a creature spawn) stand on a model floor /// instead of the terrain underneath it — the headless substitute for eyeballing a bridge/WMO /// interior deck (e.g. Deadmines) in the live client. Same synthetic-rig shape as /// `debug_assert_blink_clamp`/`debug_assert_chase_stops_at_column` (the exact @@ -519,7 +519,7 @@ pub fn debug_assert_floor_snap( Ok(()) } -/// #523 done-when: server-side proof that Blink's collision clamp lands SHORT of an obstacle +/// Done-when: server-side proof that Blink's collision clamp lands SHORT of an obstacle /// plane instead of teleporting through it — the headless substitute for a client screenshot at /// the abbey columns. Wires a synthetic 2-triangle vertical wall straight out of /// `lyracore_shared::vmap::encode` (the SAME codec `import_vmap_chunks` writes — not a shortcut @@ -668,7 +668,7 @@ pub fn debug_assert_blink_clamp(ctx: &ReducerContext, character_guid: u64) -> Re Ok(()) } -/// #525 done-when: server-side proof that a COMMITTED `nav_step` move stops at a known column +/// Done-when: server-side proof that a COMMITTED `nav_step` move stops at a known column /// instead of walking through it — the headless substitute for driving a live chase and eyeballing /// the client. Same synthetic-wall rig as `debug_assert_blink_clamp` (the exact /// `lyracore_shared::vmap::encode` codec, duplicated across every cell the wall's AABB could @@ -937,7 +937,7 @@ pub fn debug_assert_unreachable_goal_stops_at_wall( Ok(()) } -/// #522 benchmark harness: run `directions` synthetic LoS queries around EVERY real creature +/// Benchmark harness: run `directions` synthetic LoS queries around EVERY real creature /// (`type_mask::CREATURE`) inside `[min_x,max_x]×[min_y,max_y]` on `map` — one probe segment per /// creature per compass direction at `radius` yd, i.e. exactly the (creature × in-range-candidate) /// shape `best_aggro_target` walks on a sense tick (`sense.rs`), scaled to a box of real, @@ -1005,14 +1005,14 @@ pub fn debug_bench_los( ); } -/// #525 perf-risk companion to `debug_bench_los`: `nav_step`'s collision gate (`vmap::collision_ray`) +/// Perf-risk companion to `debug_bench_los`: `nav_step`'s collision gate (`vmap::collision_ray`) /// runs on every COMMITTED movement step (chase/return/wander/flee/pet-follow — the 500ms tick), -/// not the 4s sense tick #522 measured, so it pays the exact-ray cost 8x more often per mover. Same +/// not the 4s sense tick the LoS benchmark measured, so it pays the exact-ray cost 8x more often per mover. Same /// shape as `debug_bench_los` — one probe per real creature per compass direction, `radius` yd out /// — but calls `nav::nav_step` (the actual committed-step choke point) instead of a raw LoS ray, so /// the query volume this produces is directly comparable against `debug_bench_los`'s numbers on the /// SAME box: if this reducer's wall time isn't roughly `directions`× cheaper per creature than -/// `debug_bench_los(exact=true)`, the per-tick multiplication the #525 review flagged is real and +/// `debug_bench_los(exact=true)`, the per-tick multiplication the review flagged is real and /// needs the "cheap PK existence check" fallback the issue text proposed. `gated` counts how many /// probes the ray actually truncated (checksum, not just speed, so two runs can be diffed for /// correctness too). @@ -1073,7 +1073,7 @@ pub fn debug_grant_default_actions( ctx: &ReducerContext, character_name: String, ) -> Result<(), String> { - // REFUSE verdict (issue #30) — harness writers get the same fence as production ones. + // REFUSE verdict — harness writers get the same fence as production ones. let c = crate::helpers::character_by_name(ctx, &character_name) .ok_or_else(|| format!("no character named {character_name}"))?; crate::action_bar::grant_createinfo_actions(ctx, c.guid, c.owner_identity, c.race, c.class); @@ -1091,7 +1091,7 @@ pub fn debug_backfill_go_grid(ctx: &ReducerContext) { for guid in all { if let Some(mut g) = gos.guid().find(guid) { let (gx, gy) = lyracore_shared::spatial::grid_cell(g.x, g.y); - // #456: `cell` too — see `debug_regrid`'s note; a migrated row's grid is already right. + // `cell` too — see `debug_regrid`'s note; a migrated row's grid is already right. if g.grid_x != gx || g.grid_y != gy || g.cell != lyracore_shared::spatial::grid_cell_id(gx, gy) diff --git a/module/src/debug/mod.rs b/module/src/debug/mod.rs index fa6cb85a4..475da4b90 100644 --- a/module/src/debug/mod.rs +++ b/module/src/debug/mod.rs @@ -22,12 +22,12 @@ //! MUST NOT use `entity_by_owner(ctx, ctx.sender())` (that only resolves the gateway's per-player //! connection identity). This is the single most important difference from the player reducers. //! -//! # LAYOUT (#386) -//! Split along the section banners after the #378 reseed/rearm collapse: this file (`mod`) holds the +//! # LAYOUT +//! Split along the section banners after the reseed/rearm collapse: this file (`mod`) holds the //! world/combat/item levers + the shared `equip_into` helper; `readout` is the `DebugReadout` table + //! its two writers; `audit` is the class-kit/quest-chain content audits; `repair` is the consolidated //! post-publish repair pass; `encounter`/`instance` are the work-item 228/190 operator levers; and -//! `fingerprint` is the catalogue-parity content hash (#82). `lib.rs`'s single `#[cfg(feature = +//! `fingerprint` is the catalogue-parity content hash. `lib.rs`'s single `#[cfg(feature = //! "debug_reducers")] mod debug;` gates the whole directory unchanged — a directory module resolves //! identically to a single file as far as that `cfg` and the crate-root `pub use debug::*;` are //! concerned. @@ -200,7 +200,7 @@ pub fn debug_apply_damage( /// /// Routes through the SAME shared kill path combat uses (`combat::kill_player` / `combat::kill_creature`) /// so a debug kill is byte-identical to a real killing blow, including channel teardown and combat- -/// deadline zeroing — not a re-expression of the swing-loop kill logic (#386: this doc used to sit, +/// deadline zeroing — not a re-expression of the swing-loop kill logic (this doc used to sit, /// misattached, above `debug_apply_damage`, and claimed "the combat module has no reusable kill fn", /// which the body below has not been true of since `combat::kill_player`/`kill_creature` landed). #[reducer] @@ -385,7 +385,7 @@ pub fn debug_spawn_player_entity(ctx: &ReducerContext, character_guid: u64) -> R return Ok(()); } - // REFUSE verdict (issue #30). This is `player_login`'s RE-MATERIALISATION path wearing a harness + // REFUSE verdict. This is `player_login`'s RE-MATERIALISATION path wearing a harness // hat: it builds a live entity straight off the durable row, so without the fence it is a second // way to put a live copy back on a shard the character has left — the dual-liveness dupe the // escrow exists to prevent. `player_login` fences it; so must this. @@ -453,7 +453,7 @@ pub fn debug_set_level( character_guid: u64, level: u32, ) -> Result<(), String> { - // REFUSE verdict (issue #30) — the `debug_set_money` sibling. `set_character_level` writes + // REFUSE verdict — the `debug_set_money` sibling. `set_character_level` writes // `Character.level` (an `ExportBlob` field) and `Character.xp` on the DURABLE row and needs no // live entity to do it (it errors only when neither an entity nor a character row exists), so // by-guid it reaches an escrowed character. Fenced here, not in `stats`: `gm_command`'s caller @@ -772,7 +772,7 @@ pub fn debug_spawn_gameobject( grid_x: lyracore_shared::spatial::grid_cell(x, y).0, grid_y: lyracore_shared::spatial::grid_cell(x, y).1, cell: lyracore_shared::spatial::cell_id_at(x, y), - // All-zero (issue #515): the debug spawn lever doesn't carry a spawn quaternion this + // All-zero: the debug spawn lever doesn't carry a spawn quaternion this // slice — the codec's yaw-only derive-from-orientation fallback covers it (orientation // is also 0.0 here, so the derived quaternion is the identity rotation, byte-consistent // with a hand-seeded fixture that never set these). @@ -1333,7 +1333,7 @@ pub fn debug_accept_quest( /// `junk_rows` used to pad the transaction with `game_creature_move_event` inserts (rows no /// subscription matched, reaped by the old 1s event TTL) as ballast for the fat-transaction/AOI-churn /// relay-drop class. That table has had no gateway subscriber since perf 2.3 (`gc.rs`), so those -/// inserts stopped being ballast and became a pure leak (#357) — deleted here, not rerouted. Kept as +/// inserts stopped being ballast and became a pure leak — deleted here, not rerouted. Kept as /// an accepted-but-ignored arg so an existing caller passing a nonzero value doesn't hit an arity /// error; it just no longer does anything. #[reducer] @@ -1377,7 +1377,7 @@ pub fn debug_expire_quest( character_guid: u64, quest_entry: u32, ) -> Result<(), String> { - // REFUSE verdict (issue #30): the write lands in `game_character_quest`, a MANIFEST table — the + // REFUSE verdict: the write lands in `game_character_quest`, a MANIFEST table — the // export blob's own enumeration — so post-begin it is a lost write cross-database. crate::helpers::require_character(ctx, character_guid)?; crate::quest::debug_force_expire(ctx, character_guid, quest_entry) @@ -1531,7 +1531,7 @@ pub fn debug_enchant_item( crate::loot::apply_enchant_item(ctx, character_guid, target_slot, enchant_id) } -/// Start `attacker_guid`'s RANGED auto-attack (#10) on the NEAREST live creature, with `spell_id` +/// Start `attacker_guid`'s RANGED auto-attack on the NEAREST live creature, with `spell_id` /// (75 Auto Shot / 5019 wand Shoot) — the small-arg test hook (a creature's own guid is a u64 > 2^53 the /// CLI `spacetime call` mangles). Requires a ranged weapon equipped (the shared gate enforces it). Mirrors /// `debug_kill_nearest`'s nearest-search, then arms the engagement via `apply_start_ranged_attack`. @@ -1552,7 +1552,7 @@ pub fn debug_ranged_attack_nearest( crate::actor::ranged_attack(ctx, attacker_guid, target_guid, spell_id) } -/// Teach `character_guid` the spell `spell_id` (#10 test hook — e.g. Auto Shot 75 on a non-Hunter so the +/// Teach `character_guid` the spell `spell_id` (test hook — e.g. Auto Shot 75 on a non-Hunter so the /// client's spellbook gets it and `CastSpellByName("Auto Shot")` works). Drives the shared /// `spell::learn_spell` by explicit guid (the player path is `ctx.sender`-bound). Needs a relog for the /// learned spell to reach the client spellbook (sent in SMSG_INITIAL_SPELLS at login). @@ -1593,10 +1593,10 @@ pub fn debug_set_xp_rate(ctx: &ReducerContext, rate: f32) -> Result<(), String> Ok(()) } -/// #456 — the REQUIRED post-publish step for the `cell` column migration. +/// The REQUIRED post-publish step for the `cell` column migration. /// /// `cell` was END-appended to the four AOI-scoped tables with `#[default(0i64)]`, and 0 is not a -/// sentinel: it is the legitimate id of cell (0, 0). So on the first publish after #456 EVERY +/// sentinel: it is the legitimate id of cell (0, 0). So on the first publish EVERY /// pre-existing row claims to live in that one cell, and the AOI subscription — which now probes /// `cell` by equality — finds nothing where those rows actually are. Moving entities self-heal on /// their next heartbeat or spline leg (sub-second), but **gameobjects are static and never re-stamp @@ -1686,7 +1686,7 @@ pub fn debug_regrid(ctx: &ReducerContext) -> Result<(), String> { .iter() .filter_map(|e| { let (gx, gy) = spatial::grid_cell(e.x, e.y); - // #456: `cell` too — a migrated row has the RIGHT grid_x/grid_y and a stale cell of 0, + // `cell` too — a migrated row has the RIGHT grid_x/grid_y and a stale cell of 0, // so comparing only the grid columns would skip every row this sweep exists to fix. (e.grid_x != gx || e.grid_y != gy || e.cell != spatial::grid_cell_id(gx, gy)) .then_some((e.guid, gx, gy)) @@ -1705,7 +1705,7 @@ pub fn debug_regrid(ctx: &ReducerContext) -> Result<(), String> { Ok(()) } -/// Grant `amount` reputation for `character_guid` with `faction_id` (#13 server-verify hook — quest-rep is +/// Grant `amount` reputation for `character_guid` with `faction_id` (server-verify hook — quest-rep is /// the only live caller, and the player turn-in path is `ctx.sender`-bound). Drives `reputation::grant_reputation` /// directly; no-ops if the faction has no rep bar / is unknown. Verify via `SELECT * FROM game_player_reputation` /// and the relayed `SMSG_SET_FACTION_STANDING`. Faction ids are small (< 2^53) so `spacetime call` carries them fine. @@ -1716,7 +1716,7 @@ pub fn debug_grant_reputation( faction_id: u32, amount: i32, ) -> Result<(), String> { - // REFUSE verdict (issue #30): `game_player_reputation` is a MANIFEST table — see + // REFUSE verdict: `game_player_reputation` is a MANIFEST table — see // `debug_expire_quest` for the same reasoning. crate::helpers::require_character(ctx, character_guid)?; crate::reputation::grant_reputation(ctx, character_guid, faction_id, amount); @@ -1989,7 +1989,7 @@ pub fn debug_set_money( copper: u32, ) -> Result<(), String> { let chars = ctx.db.game_character(); - // REFUSE verdict (issue #30) — harness writers get the same fence as production ones. + // REFUSE verdict — harness writers get the same fence as production ones. let mut c = crate::helpers::require_character(ctx, character_guid)?; c.money = copper; chars.guid().update(c); diff --git a/module/src/debug/repair.rs b/module/src/debug/repair.rs index 244086085..7907d7512 100644 --- a/module/src/debug/repair.rs +++ b/module/src/debug/repair.rs @@ -1,4 +1,4 @@ -//! The consolidated post-publish repair pass (#378) — the one reducer the publish script calls after +//! The consolidated post-publish repair pass — the one reducer the publish script calls after //! every deploy to re-seed/rearm everything `seed::init` would have inserted on a fresh database but //! that a plain auto-migrate republish silently leaves absent. @@ -14,13 +14,13 @@ use crate::{ CreatureMoveSchedule, DuelSchedule, GroundAreaSchedule, PetCareSchedule, }; -/// Consolidated post-publish repair pass (#378). SpacetimeDB's `init` reducer runs ONLY on a +/// Consolidated post-publish repair pass. SpacetimeDB's `init` reducer runs ONLY on a /// database's first-ever publish, never again on a plain (auto-migrate) republish — so every /// seed/fixture/schedule row `seed::init` inserts is silently ABSENT on an already-migrated live DB /// until something re-seeds it by hand. That "something" used to be 17 near-identical /// `debug_seed_*`/`debug_ensure_*`/`debug_rearm_*` reducers, one per fixture family — a model that /// failed operationally at least four times (aura schedule, ground-area schedule, regen fixture, -/// catalogue drift #85), and forced `debug_seed_scenario_fixtures` to bolt two extra seeders onto +/// catalogue drift), and forced `debug_seed_scenario_fixtures` to bolt two extra seeders onto /// itself as a stopgap when a live suite hard-failed on a missing fixture. Every underlying seeder /// is idempotent-if-absent by construction (each deleted reducer documented the same precedent), so /// running all of them unconditionally on every publish — fresh or auto-migrated — is always safe, @@ -71,7 +71,7 @@ pub fn debug_repair_after_publish(ctx: &ReducerContext) -> Result<(), String> { .count() as u64; crate::seed::seed_fixture_catalogue(ctx); - // Tempered Blade 5090050 + Tough Jerky 5090052 + fixture faction 50900 (issue #85) + // Tempered Blade 5090050 + Tough Jerky 5090052 + fixture faction 50900 let fixture_catalogue = [crate::seed::FIXTURE_BLADE, crate::seed::FIXTURE_JERKY] .iter() .filter(|e| ctx.db.game_item_template().entry().find(**e).is_some()) @@ -194,7 +194,7 @@ pub fn debug_repair_after_publish(ctx: &ReducerContext) -> Result<(), String> { 1 }; - // #461: ensure — never REARM — the 20 Hz movement-republish schedule. Ensure, because + // Ensure — never REARM — the 20 Hz movement-republish schedule. Ensure, because // `scheduled_at` IS the cadence knob (`set_motion_tick_ms`), and a publish must not silently // undo an operator's retune. Absent, movement stages into the private table and never relays, // so this is the row whose absence is loudest. @@ -266,7 +266,7 @@ pub fn debug_repair_after_publish(ctx: &ReducerContext) -> Result<(), String> { )), }); - // #468 stage 4a: re-arm the gateway lease reaper (bounded ghost lifetime for sessions riding + // Stage 4a: re-arm the gateway lease reaper (bounded ghost lifetime for sessions riding // the shared connection). Rearm-not-ensure is safe here — unlike the motion tick there is no // operator tuning knob to preserve; the canonical interval is the only interval. let lease_sched = ctx.db.game_gateway_lease_reaper_schedule(); diff --git a/module/src/encounter.rs b/module/src/encounter.rs index dc652106b..08cfb2f70 100644 --- a/module/src/encounter.rs +++ b/module/src/encounter.rs @@ -876,8 +876,8 @@ pub fn encounter_reset_full( /// row that would otherwise respawn into instance 0 — see [`EncounterSpawn`]), any equip row, and the /// tracking row itself. /// -/// Issue #359: this used to carry a private copy of the checklist, and that copy deleted every -/// `game_corpse_loot` row on the guid with NO `!withheld` filter — the #50 invariant, which the other +/// This used to carry a private copy of the checklist, and that copy deleted every +/// `game_corpse_loot` row on the guid with NO `!withheld` filter — the invariant, which the other /// two teardown paths honour. Wave adds are ordinary group-killable creatures and `encounter_reset` is /// the wipe handler, so a mid-roll wipe hit exactly this delete and silently ate the winner's item. fn despawn_tracked(ctx: &ReducerContext, tracked: &[EncounterSpawn]) { diff --git a/module/src/exploration.rs b/module/src/exploration.rs index d464023aa..50328f547 100644 --- a/module/src/exploration.rs +++ b/module/src/exploration.rs @@ -1,6 +1,6 @@ -//! Exploration / discovery XP (work-item 200; the "Discovered: " toast + map fog landed in -//! #41). Entering a subzone for the FIRST time awards discovery XP and sends TWO packets — this is -//! retail parity, not a duplicate (issue #89; see `check_area_exploration`'s doc comment for the +//! Exploration / discovery XP (work-item 200; the "Discovered: " toast + map fog have +//! landed). Entering a subzone for the FIRST time awards discovery XP and sends TWO packets — this is +//! retail parity, not a duplicate (see `check_area_exploration`'s doc comment for the //! cited source): `SMSG_EXPLORATION_EXPERIENCE` (the toast + fog-clear, off the //! `game_character_explored` insert) and `SMSG_LOG_XPGAIN` (the "+N experience" text, off a non-kill //! `game_xp_event` insert — the same relay kill/quest XP use). Hooked into `movement_update` @@ -54,7 +54,7 @@ crate::character_owned!(delete, fn sweep_delete_game_character_explored(ctx, cha ex.id().delete(r.id); } }); -// CROSS-DATABASE transport (issue #19): the fog-of-war map is durable progression — losing it at a +// CROSS-DATABASE transport: the fog-of-war map is durable progression — losing it at a // shard boundary would re-fog zones and re-pay discovery XP. `id` is a surrogate PK, re-minted. crate::character_owned!(transfer, fn sweep_transfer_game_character_explored(ctx, character_guid, io) { table = game_character_explored, @@ -69,7 +69,7 @@ crate::character_owned!(transfer, fn sweep_transfer_game_character_explored(ctx, /// isn't discoverable (`area_bit < 0` or `exploration_level == 0`), the discovery XP rounds to 0 /// (grey / capped), or the area was already explored. [entity] /// -/// Issue #89: a paying discovery inserts BOTH `game_character_explored` (→ the gateway's +/// A paying discovery inserts BOTH `game_character_explored` (→ the gateway's /// `SMSG_EXPLORATION_EXPERIENCE` "Discovered: " toast, carrying `experience`) AND /// `game_xp_event` (→ `SMSG_LOG_XPGAIN`, the "+N experience" text) — two packets for one grant, not a /// double-count. This is retail parity, not an artifact of this codebase: confirmed against vmangos @@ -121,7 +121,7 @@ pub(crate) fn check_area_exploration( crate::xp::grant_xp(ctx, mover, xp); // folds the GM .xprate, spends level thresholds // "+N experience" feedback via the existing SMSG_LOG_XPGAIN relay (non-kill form) — a SECOND, // deliberately overlapping packet, not a duplicate: retail sends both a discovery toast AND - // SMSG_LOG_XPGAIN for one discovery (issue #89 — see this fn's doc comment for the vmangos + // SMSG_LOG_XPGAIN for one discovery (see this fn's doc comment for the vmangos // citation). The map fog-clear and the "Discovered: " toast both ride the // game_character_explored insert above (the gateway's on_insert sets the PLAYER_EXPLORED_ZONES // bit and reads `experience` for the toast). diff --git a/module/src/gameobject.rs b/module/src/gameobject.rs index 7e71cbfb1..d00a95349 100644 --- a/module/src/gameobject.rs +++ b/module/src/gameobject.rs @@ -19,7 +19,7 @@ use crate::game_gameobject_loot; // CHEST data-driven loot table (work-item 210) use crate::game_player_skill; // GATHER skill-gate reads the gather skill row (accessor trait) use crate::game_world_entity; use crate::loot::CorpseLoot; -use crate::nav::game_nav_chunk; // arm_pool's map-fence (issue #79) — neither is wildcard-exported at the crate root +use crate::nav::game_nav_chunk; // arm_pool's map-fence — neither is wildcard-exported at the crate root use crate::terrain::game_terrain_chunk; /// The gameobject types this slice handles (cmangos `GAMEOBJECT_TYPE_*`). Deliberate @@ -205,7 +205,7 @@ pub struct GameObjectUnlocked { // sentinel on every live row, so a `1..=now` range visits only DUE nodes — the same trick // `game_aura.by_next_tick` uses. index(accessor = by_respawn_at, btree(columns = [respawn_at_micros])), - // #456: the AOI cell index — exactly 3 columns, all matched by equality terms, which is the + // The AOI cell index — exactly 3 columns, all matched by equality terms, which is the // only shape SpacetimeDB 2.7.1's subscription planner can serve (see the `cell` column). index(accessor = by_cell, btree(columns = [map_id, instance_id, cell])) )] @@ -243,7 +243,7 @@ pub struct GameObject { pub grid_x: i32, #[default(0i32)] pub grid_y: i32, - /// #456: `(grid_x, grid_y)` packed into ONE indexed value — the AOI subscription's cell key. + /// `(grid_x, grid_y)` packed into ONE indexed value — the AOI subscription's cell key. /// /// SpacetimeDB 2.7.1's subscription planner can only serve a query from an index when EVERY /// column of that index is matched by an equality term, and it skips any index with more than 3 @@ -265,7 +265,7 @@ pub struct GameObject { /// nothing ever re-stamps them on its own. That backfill is a REQUIRED post-publish step. #[default(0i64)] pub cell: i64, - /// The cmangos spawn quaternion (`gameobject.rotation0..3`) — issue #515. The client renders a + /// The cmangos spawn quaternion (`gameobject.rotation0..3`). The client renders a /// static prop's ORIENTATION from this 4-float quaternion (`GAMEOBJECT_ROTATION`, wire index 10), /// not from `orientation` alone: rot2/rot3 carry yaw, rot0/rot1 carry the terrain pitch/roll a /// bench flush against a sloped wall needs. `orientation` above still drives movement-facing math @@ -276,7 +276,7 @@ pub struct GameObject { /// `orientation` instead of sending a degenerate zero rotation — see /// `gateway/src/codec/gameobject.rs::build_gameobject_rotation_values`. /// ⚠ Field spelling: SpacetimeDB normalizes column names to snake_case, so a Rust field - /// `rotation0` is STORED as column `rotation_0`. #515 shipped as `rotation0..3` (stored + /// `rotation0` is STORED as column `rotation_0`. The import shipped it as `rotation0..3` (stored /// `rotation_0..3`); a parallel session saw the normalized live columns, took them for orphans, /// and re-declared them as `rotation_0..3` — same four columns, two spellings, briefly published /// as a duplicate-name type that broke row decode. The merge collapsed everything onto the @@ -467,7 +467,7 @@ fn activate_point(ctx: &ReducerContext, m: &GameObjectPoolMember) { grid_y: lyracore_shared::spatial::grid_cell(m.x, m.y).1, cell: lyracore_shared::spatial::cell_id_at(m.x, m.y), // Pool members carry no spawn quaternion (cmangos pool_gameobject has none either) — the - // codec derives a yaw-only quaternion from `orientation` for the all-zero case (#515). + // codec derives a yaw-only quaternion from `orientation` for the all-zero case. rotation_0: 0.0, rotation_1: 0.0, rotation_2: 0.0, @@ -476,7 +476,7 @@ fn activate_point(ctx: &ReducerContext, m: &GameObjectPoolMember) { } /// Every map id this database currently holds REAL imported spatial content for, read off -/// `game_terrain_chunk`/`game_nav_chunk` — the same ground-truth signal issue #79's fresh-shard guard +/// `game_terrain_chunk`/`game_nav_chunk` — the same ground-truth signal the fresh-shard guard /// uses in `importer/scripts/import-world.sh`'s `db_imported_probe`. `init` seeds neither table (only a real /// `importer --apply` run ever writes to them; see both files' module doc comments), so an EMPTY /// result here means this database has never received a real import — everything it holds is `init`'s @@ -496,7 +496,7 @@ fn imported_terrain_maps(ctx: &ReducerContext) -> HashSet { /// call (and a bare-dev-DB `debug_setup_gather_pool`) must still arm every pool — this admits /// everything in that case. Once ANY map has real terrain/nav content, a member on a DIFFERENT map is /// foreign: refusing it is what stops `arm_all_pools` resurrecting another continent's fixture after -/// that continent's own import already wiped it (issue #79 — the false REGRESSION a clean map-1 import +/// that continent's own import already wiped it (the false REGRESSION a clean map-1 import /// used to end with, because the demo tier-pool's map-0 Northshire point got re-armed regardless). pub(crate) fn pool_member_map_is_live(member_map: u32, imported_maps: &HashSet) -> bool { imported_maps.is_empty() || imported_maps.contains(&member_map) @@ -507,7 +507,7 @@ pub(crate) fn pool_member_map_is_live(member_map: u32, imported_maps: &HashSet bool { .is_none() } -/// The GO target-acquisition gate shared by [`apply_pick_lock`] and [`apply_use_gameobject`] (issue -/// #372): resolve the GameObject + its template, resolve the caster's live entity, and confirm they +/// The GO target-acquisition gate shared by [`apply_pick_lock`] and [`apply_use_gameobject`]: +/// resolve the GameObject + its template, resolve the caster's live entity, and confirm they /// share a map+instance and are within [`USE_RANGE_SQ`]. Every error string below is preserved verbatim /// from both call sites (the two ~30-line blocks were byte-identical before this extraction). fn usable_go( @@ -1119,7 +1119,7 @@ pub fn debug_pick_lock_entry( /// `import_gameobjects_append` in the SAME commit, not a table migration: this is a string protocol /// between the importer and this reducer, not a reducer arg) lets a DOOR/BUTTON spawn already-open /// (cmangos `startOpen`); every other type's importer row carries 0 (ready/closed), byte-identical to -/// the pre-211 always-0 shape. `rot0..3` (issue #515, END fields — the cmangos spawn quaternion) ride +/// the pre-211 always-0 shape. `rot0..3` (END fields — the cmangos spawn quaternion) ride /// AFTER `initial_state` rather than beside `orientation` so every pre-515 packed-row builder in this /// file's tests keeps working unmodified; a hand-built row that omits them fails loudly (field-count /// check below) rather than silently importing a zero quaternion. @@ -1151,7 +1151,7 @@ pub fn import_gameobjects_append(ctx: &ReducerContext, packed: String) -> Result /// `guid,template_entry,map,x,y,z,o,initial_state,rot0,rot1,rot2,rot3`) into `game_gameobject` rows /// (stamped at `ctx.timestamp`), returning the count loaded. Shared by both reducers above. /// `initial_state` is work-item 211's DOOR/BUTTON `startOpen` carry — every non-door/button importer -/// row still sends 0, so existing content is byte-identical. `rot0..3` (issue #515) is the cmangos +/// row still sends 0, so existing content is byte-identical. `rot0..3` is the cmangos /// spawn quaternion; the importer sends the dump's literal `0,0,0,0` for any row it doesn't have one /// for, so this reducer never needs to derive a fallback itself — that derivation is the WIRE codec's /// job (`gateway/src/codec/gameobject.rs`), not storage's. @@ -1171,7 +1171,7 @@ fn load_go_batch(ctx: &ReducerContext, packed: &str) -> Result { let pu32 = |s: &str| s.parse::().map_err(|_| format!("bad u32: {s}")); let pf32 = |s: &str| s.parse::().map_err(|_| format!("bad f32: {s}")); let pu8 = |s: &str| s.parse::().map_err(|_| format!("bad u8 state: {s}")); - // Parse the position ONCE (#456): the grid columns used to re-parse `f[3]`/`f[4]` per + // Parse the position ONCE: the grid columns used to re-parse `f[3]`/`f[4]` per // component, and `cell` would have made that four parses of the same two fields. Hoisting // also makes it structurally impossible for `grid_x`, `grid_y` and `cell` below to be // derived from different coordinates. @@ -1362,7 +1362,7 @@ mod tests { assert_eq!(respawn_window_micros(180), RESPAWN_WINDOW_MICROS); // 180s == the const, no surprise } - /// Issue #79 — `arm_pool`'s map-fence. An EMPTY imported-maps set (nothing has ever been really + /// `arm_pool`'s map-fence. An EMPTY imported-maps set (nothing has ever been really /// imported into this database) must admit every member regardless of its map: that's `init`'s own /// publish-time arm, and a bare-dev-DB `debug_setup_gather_pool`, and both must keep working exactly /// as before this fence existed. @@ -1409,7 +1409,7 @@ mod tests { assert!(!pool_member_map_is_live(1, &multi_map)); } - /// `eligible_pool_candidates` — the ACTUAL splice `arm_pool` calls each iteration (issue #79 review: + /// `eligible_pool_candidates` — the ACTUAL splice `arm_pool` calls each iteration (review: /// the disclosed gap was that only the isolated `pool_member_map_is_live` predicate had a unit test, /// not the wiring; this exercises the real production filter — weight>0 AND live-map AND not-already- /// active — together, exactly as `arm_pool` composes them). diff --git a/module/src/gc.rs b/module/src/gc.rs index 9c8471844..a413918ef 100644 --- a/module/src/gc.rs +++ b/module/src/gc.rs @@ -64,7 +64,7 @@ pub fn reap_movement_events(ctx: &ReducerContext, _schedule: EventReaperSchedule } }}; } - // `game_movement_event` is GONE (dropped — #350): peer movement rides the per-mover + // `game_movement_event` is GONE (dropped): peer movement rides the per-mover // `game_entity_motion` row, which is updated in place and needs no reaping at all (perf catalog // 2.1). This reap was the second half of the O(C²) cost: at 200 co-located players it deleted // 67,753 rows/s to match the 70,568 the writer had just inserted. @@ -86,18 +86,18 @@ pub fn reap_movement_events(ctx: &ReducerContext, _schedule: EventReaperSchedule reap!(game_addon_message); // addon-bridge UI messages (184, RLS-scoped) reap!(game_roll_event); // /roll broadcast results reap!(game_group_event); // group invite/roster notifications (RLS-scoped) - reap!(game_trade_event); // trade-status relay rows (#120, RLS-scoped) + reap!(game_trade_event); // trade-status relay rows (RLS-scoped) reap!(game_duel_event); // Duel lifecycle relay rows (RLS-scoped) - reap!(game_bot_invite_intent); // bot-decided invites awaiting gateway pickup (issue #54) + reap!(game_bot_invite_intent); // bot-decided invites awaiting gateway pickup reap!(game_bot_transfer_intent); // bot-decided Shard crossings awaiting gateway pickup - reap!(game_movement_violation); // recent anti-cheat diagnostics (issue #211) - // Rest-area zzz/blue-bar relay rows (196). Caught missing by the #379 gc_reap_tripwire: this + reap!(game_movement_violation); // recent anti-cheat diagnostics + // Rest-area zzz/blue-bar relay rows (196). Caught missing by the gc_reap_tripwire: this // table carries the same `id: u64` + `created_at: Timestamp` TTL shape as every table above but // had no reap line — every inn threshold crossing for the lifetime of a character left one more // row behind. The durable rest state (`Character.resting`/`rested_xp`) lives elsewhere; this row // is only the one-shot PLAYER_BYTES_2 relay. reap!(game_rest_state_event); - reap!(game_breath_relay_event); // breath timer edges + drowning damage relay (#141) + reap!(game_breath_relay_event); // breath timer edges + drowning damage relay // Never-answered pending invites. Same id+created_at shape as the event tables, but on // the longer INVITE_TTL (a human is looking at the invite dialog). After that the row is dead @@ -116,7 +116,7 @@ pub fn reap_movement_events(ctx: &ReducerContext, _schedule: EventReaperSchedule } } - // Idle Trade Sessions (#123): `created_at` is bumped by every trade action, so the pure + // Idle Trade Sessions: `created_at` is bumped by every trade action, so the pure // policy (`trade::session_is_stale`) measures idleness — a live negotiation is never reaped. // Torn down THROUGH `cancel_trade_for`, never a bare delete, so both clients hear // `TradeCanceled` instead of keeping stale windows open. diff --git a/module/src/gm.rs b/module/src/gm.rs index 6cacacc85..402c4f865 100644 --- a/module/src/gm.rs +++ b/module/src/gm.rs @@ -294,7 +294,7 @@ pub(crate) fn parse_gm_command(text: &str) -> Result { pub fn set_gm_level(ctx: &ReducerContext, character_name: String, level: u8) -> Result<(), String> { require_operator(ctx)?; let chars = ctx.db.game_character(); - // REFUSE verdict (issue #30): an in-transit character reads as absent, so this operator write + // REFUSE verdict: an in-transit character reads as absent, so this operator write // cannot land on a source copy the destination already serialized past. let mut c = crate::helpers::character_by_name(ctx, &character_name) .ok_or_else(|| format!("no player named {character_name}"))?; diff --git a/module/src/graveyard.rs b/module/src/graveyard.rs index 3968498f8..b627ae55d 100644 --- a/module/src/graveyard.rs +++ b/module/src/graveyard.rs @@ -1,5 +1,5 @@ //! Graveyard resolution (work-item 209/226): the death-release subsystem `world::do_repop` calls to -//! pick where a ghost teleports. Extracted from `world.rs` (issue #385) — it used to be an inline +//! pick where a ghost teleports. Extracted from `world.rs` — it used to be an inline //! `mod graveyard` sitting inside the player-entity file, a complete subsystem (five consts, zone //! resolution, faction teams, the instance-map arm, three candidate builders, two pickers) plus its //! own ~250-line test battery, living inside a 2,700-line file it had nothing else to do with. @@ -140,7 +140,7 @@ const STATIC_CANDIDATES: [Graveyard; 5] = [ /// The instance map's own zone id (cmangos `game_graveyard_zone.ghost_zone` for deaths inside) — /// the map→zone hop `terrain::zone_id_at` can't make without terrain. `None` = not an instance map → -/// the normal open-world chain runs untouched. Issue #376: a field read off +/// the normal open-world chain runs untouched. A field read off /// `crate::instance::DUNGEON_MAPS`, the one dungeon-detail record shared with /// `instance_static_fallback` below and `crate::instance::entrance_fallback` — see that table's /// doc for the Deadmines/zone-1581 provenance note this used to carry directly. @@ -152,7 +152,7 @@ pub(crate) fn instance_release_zone(map_id: u32) -> Option { /// state): Deadmines releases at Sentinel Hill (the Westfall graveyard nearest the Moonbrook /// entrance in spirit — the exact safe loc is `[V]`, same provenance caveat as the const itself). /// NEVER fall through to `nearest(px, py)` for an instance map — cross-map 2-D distance against -/// the static consts is meaningless (see the section comment above). Issue #376: a field read +/// the static consts is meaningless (see the section comment above). A field read /// off `crate::instance::DUNGEON_MAPS` (see [`instance_release_zone`]). pub(crate) fn instance_static_fallback(map_id: u32) -> Option { crate::instance::dungeon(map_id).map(|d| { @@ -223,7 +223,7 @@ pub(crate) fn pick_graveyard( pub(crate) use lyracore_shared::faction::team_for_race; /// Convert one `game_graveyard` row into the pure [`Graveyard`] shape every candidate source -/// converges on — the identical 7-line map every caller below used to repeat inline (issue #385). +/// converges on — the identical 7-line map every caller below used to repeat inline. fn to_gy(g: GraveyardLoc) -> Graveyard { Graveyard { map: g.map_id, @@ -242,7 +242,7 @@ fn to_gy(g: GraveyardLoc) -> Graveyard { /// `map_filter`: `Some(map_id)` restricts to that map — the open-world call (never cross-map; see /// the RESOLVED note below). `None` doesn't filter — the instance-release arm (work-item 226): a /// dungeon zone's linked graveyard is deliberately on ANOTHER map (Deadmines zone → Westfall, map -/// 0), which is precisely what the map filter exists to prevent for open-world zones. Issue #385: +/// 0), which is precisely what the map filter exists to prevent for open-world zones.: /// this used to be two near-identical functions (`zone_linked`/`zone_linked_cross_map`, the second /// the first minus one `.filter()`); collapsed into one. // NOTE (190 slice 2, RESOLVED — no instance gate needed here): graveyard data is static, @@ -297,7 +297,7 @@ pub(crate) fn resolve_graveyard( // falling back to the per-map static const when nothing is imported. Handled BEFORE the // open-world chain, whose every step (terrain zone resolve / same-map candidates / raw // nearest-const) is meaningless inside a WMO map. - // Issue #376: the "map_id != 0 with no instance arm" runtime `warn!` this branch used to + // The "map_id != 0 with no instance arm" runtime `warn!` this branch used to // carry is gone — it existed because `instance_release_zone` and `instance_static_fallback` // used to be TWO independently-hand-matched functions that could disagree on which maps were // "instance maps", and the warn was the only way that disagreement surfaced (a garbled diff --git a/module/src/group.rs b/module/src/group.rs index 8855758b6..d22633012 100644 --- a/module/src/group.rs +++ b/module/src/group.rs @@ -9,17 +9,17 @@ //! events carry the roster snapshot in their payload, built in the SAME transaction as the //! membership change (a relay-time coordinator read would race the event across connections). //! -//! **Where this state LIVES (issue #22, group slice).** `game_group` / `game_group_member` / +//! **Where this state LIVES (group slice).** `game_group` / `game_group_member` / //! `game_group_invite` are authoritative on **realm-core** — none of the three is coupled to space, //! and all three broke the moment a second database existed (a player inside Deadmines could not //! invite one in Elwynn: `invite_core` resolved the target inside the CALLING database and a //! character on another shard has no row there). The gateway drives them there through the //! operator-gated [`realm_group_op`], and each world shard's copy is a write-through cache it -//! refreshes with [`sync_group_mirror`] — the `game_account`/`game_session` relationship from #20. +//! refreshes with [`sync_group_mirror`] — the `game_account`/`game_session` relationship. //! A single-database deployment has no realm-core to route to and keeps calling the player-facing //! reducers below exactly as before; the whole split is a gateway routing decision, not a schema one. //! -//! **Server-driven invites (issue #54).** A playerbot's serendipity invite has no client and no +//! **Server-driven invites.** A playerbot's serendipity invite has no client and no //! `ctx.sender()` to resolve — it is a decision the module's own goal tick makes. It cannot write //! group rows for the same reason a player's cross-shard invite cannot: only the gateway can reach //! realm-core. [`BotInviteIntent`] is the module's half of that split — a DECISION, not a write — @@ -125,33 +125,32 @@ crate::character_owned!(restamp, fn sweep_restamp_game_group_member(ctx, charact } }); -// CROSS-DATABASE transport (issue #22, group slice): membership does NOT ride the export blob. +// CROSS-DATABASE transport (group slice): membership does NOT ride the export blob. // -// #19 shipped an interim MIRROR here — the member row travelled in the manifest carrying its +// Shipped an interim MIRROR here — the member row travelled in the manifest carrying its // original `group_id`, and an `ensure_group` helper re-created the parent `game_group` row at the // destination. Its own AC#3 called for that mirror to be deleted once membership had one -// authoritative home, and #22 gives it one: `game_group` / `game_group_member` / `game_group_invite` +// authoritative home, and the group slice gives it one: `game_group` / `game_group_member` / `game_group_invite` // are authoritative on REALM-CORE, and each world shard's copy is a gateway-maintained write-through -// cache (`sync_group_mirror` below) — the same relationship `game_account` / `game_session` have had -// since #20. +// cache (`sync_group_mirror` below) — the same relationship `game_account` / `game_session` have. // // So the blob must not carry membership: it would race the authority. The gateway re-pushes the // realm-core roster onto the destination at world entry (`world::party::on_world_entry`), which is // strictly better than the snapshot — a party SPLIT across the boundary re-syncs both sides, where // the snapshot could only carry what the character had when it stepped into the portal. // -// This also settles the three OPEN entries on the in-transit exception list (`transfer/mod.rs`, issue -// #30): a third party's accept/kick/leave for an in-transit character is now a REALM-CORE write, so +// This also settles the three OPEN entries on the in-transit exception list (`transfer/mod.rs`): +// a third party's accept/kick/leave for an in-transit character is now a REALM-CORE write, so // there is no source-copy write left to lose. crate::character_owned!(not_transported, fn sweep_transfer_game_group_member()); /// Drop a character's MIRROR row on the shard it is leaving, without the leave/disband semantics -/// (issue #19, re-scoped by #22). +/// (re-scoped). /// /// `transfer::do_finish` calls this immediately before `cascade_delete_character` tears the source /// copy down. A shard hop is not a departure: running `remove_member` would fire DESTROYED at the /// remaining members and DISBAND a two-person party the moment its first member stepped through the -/// portal. Under #22 that would be worse than wrong — it would be a MIRROR inventing a membership +/// portal. On realm-core that would be worse than wrong — it would be a MIRROR inventing a membership /// change and notifying clients about it, when the authority (realm-core) recorded nothing at all. /// Deleting the row and nothing else is exactly what a departing cache entry should do. /// @@ -201,14 +200,14 @@ crate::character_owned!(delete, fn sweep_delete_game_group_invite(ctx, character invites.id().delete(id); } }); -// CROSS-DATABASE transport (issue #19): a pending invite is a 2-minute dialog with a GC TTL, and +// CROSS-DATABASE transport: a pending invite is a 2-minute dialog with a GC TTL, and // the inviter is by definition NOT transferring with the target (they are still standing in the // open world). Carrying it would pop an invite dialog for a party the arriving character cannot see. // Not transported, by decision — the invite dies with the source copy, exactly as a decline would. crate::character_owned!(not_transported, fn sweep_transfer_game_group_invite()); -/// A bot-initiated invite the MODULE has DECIDED but cannot execute (issue #54, closing the gap -/// #22's group slice opened): `game_group`/`game_group_member` are authoritative on realm-core, and +/// A bot-initiated invite the MODULE has DECIDED but cannot execute (closing the gap +/// the group slice opened): `game_group`/`game_group_member` are authoritative on realm-core, and /// only the gateway can reach it — the module never has. Before this table existed, playerbots' /// serendipity invite (`packages/playerbots/src/goals.rs`, `maybe_invite_fellow_quester`) called /// [`invite_core`] directly, writing this shard's LOCAL `game_group`/`game_group_member` rows, which @@ -248,7 +247,7 @@ pub struct BotInviteIntent { pub op: u8, } -/// Record a bot's serendipity invite DECISION for the gateway to execute (issue #54). No gating here +/// Record a bot's serendipity invite DECISION for the gateway to execute. No gating here /// beyond existence-of-nothing — every real gate (leader-only, party cap, already-grouped, /// pending-invite-replaces-older) lives in `invite_core_on`/`realm_group_op`, which the gateway calls /// against the correct authority; this is a pure write, mirroring how a player's own CMSG_GROUP_INVITE @@ -326,8 +325,8 @@ pub struct GroupEvent { // column (String cannot be #[default]-ed — the macro's typecheck is const): fine because this // whole table has no pre-payload row to migrate anywhere real. pub payload: String, - /// The recipient's CHARACTER GUID (issue #22, group slice). END-appended + `#[default(0)]`, so - /// this is an additive auto-migration and every pre-#22 row reads as 0. + /// The recipient's CHARACTER GUID (group slice). END-appended + `#[default(0)]`, so + /// this is an additive auto-migration and every earlier row reads as 0. /// /// `recipient_identity` cannot address a recipient on REALM-CORE: an identity is minted per /// (account, database) by the node, so the identity a player holds on a world shard names @@ -351,7 +350,7 @@ pub(crate) fn push_event( other_guid: u64, payload: String, ) { - // #22: no longer an early return on a missing character row. On a world shard the row is there + // No longer an early return on a missing character row. On a world shard the row is there // and this is byte-identical to before; on REALM-CORE there are no character rows at all, and // returning early there would mean the directory database could never notify anybody — every // invite popup and roster refresh for a cross-shard party would be dropped at the source. @@ -430,7 +429,7 @@ pub(crate) fn group_of(ctx: &ReducerContext, character_guid: u64) -> Option R Ok(()) } -/// The identity-free decline core (#22): the body `group_decline` used to inline, so the realm-core +/// The identity-free decline core: the body `group_decline` used to inline, so the realm-core /// plane runs the SAME code rather than a second implementation of it. pub(crate) fn decline_invite_for(ctx: &ReducerContext, decliner_guid: u64) -> Result<(), String> { let invites = ctx.db.game_group_invite(); @@ -708,7 +707,7 @@ pub(crate) fn decline_invite_for(ctx: &ReducerContext, decliner_guid: u64) -> Re Ok(()) } -/// The identity-free leave core (#22) — the body `group_leave` used to inline. +/// The identity-free leave core — the body `group_leave` used to inline. pub(crate) fn leave_group_for(ctx: &ReducerContext, leaver_guid: u64) -> Result<(), String> { if group_of(ctx, leaver_guid).is_none() { return Err(group_err::NOT_IN_GROUP.to_string()); @@ -717,7 +716,7 @@ pub(crate) fn leave_group_for(ctx: &ReducerContext, leaver_guid: u64) -> Result< Ok(()) } -/// The identity-free kick core (#22) — the body `group_uninvite` used to inline. +/// The identity-free kick core — the body `group_uninvite` used to inline. pub(crate) fn uninvite_from_group( ctx: &ReducerContext, leader_guid: u64, @@ -736,7 +735,7 @@ pub(crate) fn uninvite_from_group( Ok(()) } -/// The identity-free loot-method core (#22) — the body `group_loot_method` used to inline. +/// The identity-free loot-method core — the body `group_loot_method` used to inline. pub(crate) fn set_loot_method_for( ctx: &ReducerContext, leader_guid: u64, @@ -881,10 +880,10 @@ pub(crate) fn leader_after_removal( } // =========================================================================================== -// REALM-CORE plane (issue #22, group slice) +// REALM-CORE plane (group slice) // =========================================================================================== -/// Which DEPLOYMENT of this one module a group call is running on (issue #22). +/// Which DEPLOYMENT of this one module a group call is running on. /// /// Realm-core is not a different crate — it is this same wasm published under a second database name /// (`realm_core.rs`'s header). Every table exists on both; what differs is which rows are ever @@ -936,7 +935,7 @@ pub fn realm_group_op( } } -/// Replace this database's MIRROR of one party with realm-core's authoritative roster (issue #22). +/// Replace this database's MIRROR of one party with realm-core's authoritative roster. /// /// The world shards keep their `game_group` / `game_group_member` tables — the constraint is that a /// live deployment cannot drop a table, and the invalidation story is the better answer anyway: @@ -945,11 +944,11 @@ pub fn realm_group_op( /// `instance.rs`) resolve membership through `group_of`/`members_of` against the LOCAL rows, and /// every one of them is a hot-path read that must not become a cross-database call. So the shard's /// copy becomes a gateway-maintained write-through cache of realm-core's, exactly as `game_account` -/// and `game_session` became caches of realm-core's copies in #20. +/// and `game_session` became caches of realm-core's copies. /// /// Invalidation: the gateway re-pushes this after every party op it runs (to every connected world /// shard) and at every world entry (to the shard the character just entered — which is what carries -/// a party across a shard boundary now that the #19 blob mirror is gone). A shard that misses a push +/// a party across a shard boundary now that the blob mirror is gone). A shard that misses a push /// re-syncs on the next of either. Nothing here NOTIFIES anybody: the realm-core plane owns the /// client-facing relay, and a mirror that pushed its own `SMSG_GROUP_LIST` would double-render the /// party frame on the acting player's shard. @@ -1199,7 +1198,7 @@ mod tests { assert!(!valid_loot_threshold(255)); } - // ---- The realm-core mirror (issue #22, group slice) ---- + // ---- The realm-core mirror (group slice) ---- #[test] fn the_mirror_diff_deletes_departed_rows_and_inserts_arriving_members() { @@ -1221,14 +1220,14 @@ mod tests { assert_eq!(mirror_plan(&[], &[100, 200]), (vec![], vec![100, 200])); } - // event_recipient_identity's pinned test moved to helpers.rs with the function itself (issue #371). + // event_recipient_identity's pinned test moved to helpers.rs with the function itself. // A reducer body needs a live `ReducerContext`, so the authorization and dispatch decisions // below use narrow Architecture Tests. /// The `//`-stripped body of `signature`'s function — assert on CODE, never on the prose beside /// it. Shared with every other file's copy of this scan as [`crate::test_scan::code_of`] - /// (issue #64 — this used to be six near-identical, drifted-apart copies). + /// (this used to be six near-identical, drifted-apart copies). use crate::test_scan::code_of; /// **The operator gate is the entire authorization of Gateway-driven party work.** @@ -1244,7 +1243,7 @@ mod tests { "pub fn sync_group_mirror(", ] { let body = code_of(include_str!("group.rs"), f); - // The FIRST STATEMENT, not merely present (review of PR #56). A bare `contains` was + // The FIRST STATEMENT, not merely present (review). A bare `contains` was // satisfied by a gate that never runs: wrapping the line in `if false { … }` — the // tripwire defeat this batch has already documented twice — left all 521 module tests // green with `realm_group_op` completely ungated, and so would `let _ = …` or an early diff --git a/module/src/gw.rs b/module/src/gw.rs index 4ff09a605..4f72edbce 100644 --- a/module/src/gw.rs +++ b/module/src/gw.rs @@ -1,4 +1,4 @@ -//! The TRUSTED GATEWAY verb surface (#468 stage 4a). +//! The TRUSTED GATEWAY verb surface (stage 4a). //! //! One reducer family — `gw_(ctx, actor_guid, ...)` — for a gateway that holds a SHARED //! SpacetimeDB connection instead of one connection per player. The sender-shaped player reducers @@ -17,7 +17,7 @@ //! (`world::apply_movement_update`, the `actor.rs` verbs, ...). Gates live in the cores and //! cannot drift between the two entries. //! -//! These are THE player-verb surface (#483): the sender-shaped (`ctx.sender`-authorized) twins +//! These are THE player-verb surface: the sender-shaped (`ctx.sender`-authorized) twins //! are deleted, and every player action reaches the module through a `gw_*` verb on the //! gateway's privileged connection. @@ -31,7 +31,7 @@ use crate::helpers::{acting_entity_by_guid, require_operator}; // The gateway lease — bounded ghost lifetime for shared-connection sessions [server] // =========================================================================================== // -// THE #468 sharp edge: under one shared connection, a gateway crash fires ONE +// THE sharp edge: under one shared connection, a gateway crash fires ONE // `client_disconnected` instead of one per player, so nothing per-player tears world entities // down and a crash would leave every seated player as a permanent world ghost. The fix is a // LEASE: each gateway heartbeats its row; sessions opened through the shared connection bind @@ -39,7 +39,7 @@ use crate::helpers::{acting_entity_by_guid, require_operator}; // scheduled reaper removes the world entities of any lease that stops heartbeating. // // Deliberately TTL-ONLY — no sweep on the shared connection's own disconnect. The coordinator -// connection reconnects routinely (module republish closes every websocket, #278), and a +// connection reconnects routinely (module republish closes every websocket), and a // disconnect-triggered sweep would mass-despawn every player on a live gateway that is merely // reconnecting. A dead gateway simply stops heartbeating and lapses; ghost lifetime is bounded // by TTL + reap interval (~75 s worst case) no matter HOW the gateway died. @@ -66,7 +66,7 @@ pub struct GatewaySession { } /// The reaper's schedule row — armed by `seed::init` on a fresh database and ensured by -/// `debug_repair_after_publish` on every publish (the #465 lesson: `init` never re-runs on an +/// `debug_repair_after_publish` on every publish (the lesson: `init` never re-runs on an /// auto-migrate republish). [server] #[table(accessor = game_gateway_lease_reaper_schedule, scheduled(reap_gateway_leases))] pub struct GatewayLeaseReaperSchedule { @@ -197,7 +197,7 @@ pub fn gw_movement_update( crate::world::apply_movement_update(ctx, mover, opcode, movement_info, x, y, z, o, move_time_ms) } -/// One movement update inside a [`gw_movement_batch`] call (#482). Field-for-field the +/// One movement update inside a [`gw_movement_batch`] call. Field-for-field the /// `gw_movement_update` argument list with the actor made explicit per entry. #[derive(spacetimedb::SpacetimeType)] pub struct GwMove { @@ -231,7 +231,7 @@ fn apply_movement_batch( } } -/// #482: ONE transaction per gateway tick instead of one per player heartbeat. The measured wall +/// ONE transaction per gateway tick instead of one per player heartbeat. The measured wall /// behind this: ~10k `gw_movement_update` transactions/s at ~43µs of per-transaction machinery /// each put the writer at 92% while the actual row work (the batched `publish_motion` side) was /// a fraction of that. The gateway collects a tick's worth of movement (25–50ms) and sends it @@ -259,8 +259,8 @@ pub fn gw_movement_batch(ctx: &ReducerContext, moves: Vec) -> Result<(), ) }, ); - // #482: drain the staged motion INSIDE this transaction — the subscription sweep is paid - // once per batch either way (#461's whole point), and inline draining removes the publish + // Drain the staged motion INSIDE this transaction — the subscription sweep is paid + // once per batch either way (the whole point), and inline draining removes the publish // schedule's 50ms latency quantum plus the pending-row churn for this path. crate::motion::publish_staged(ctx); Ok(()) @@ -293,7 +293,7 @@ pub fn gw_cast_at( /// [`crate::spell::scheduler::apply_cast_spell_at`] behind the gateway gate — the GROUND-TARGET /// cast form (`CMSG`-routed `cast_spell_at`): spellbook + NaN + self-target-default gates, target -/// coords carried. Distinct from [`gw_cast_at`], which is the unit-target instant core (#479). +/// coords carried. Distinct from [`gw_cast_at`], which is the unit-target instant core. #[reducer] pub fn gw_cast_spell_at( ctx: &ReducerContext, @@ -357,7 +357,7 @@ pub fn gw_stop_attack(ctx: &ReducerContext, actor_guid: u64) -> Result<(), Strin } /// [`crate::world::apply_set_sheathed`] behind the gateway gate — the `CMSG_SETSHEATHED` a client -/// sends on `Z`. `state` is raw client input and is range-checked in the apply fn, not here. [#101] +/// sends on `Z`. `state` is raw client input and is range-checked in the apply fn, not here. #[reducer] pub fn gw_set_sheathed(ctx: &ReducerContext, actor_guid: u64, state: u8) -> Result<(), String> { require_operator(ctx)?; @@ -559,7 +559,7 @@ pub fn gw_send_emote( crate::chat::apply_send_emote(ctx, sender, text_emote, emote_anim, target_guid) } -/// The shared-connection login (#468 stage 4d): enter the world with the account named by id and +/// The shared-connection login (stage 4d): enter the world with the account named by id and /// the session bound to this gateway's lease. Delegates to the same [`crate::world::apply_player_login`] /// core the sender path uses; the OWNER identity stamped onto the entity and the character's /// owner-RLS rows is the account's BOUND identity (from `establish_session`), so a per-player @@ -627,7 +627,7 @@ pub fn gw_leave_world(ctx: &ReducerContext, actor_guid: u64) -> Result<(), Strin } // =========================================================================================== -// Chat / social (#479 batch B) +// Chat / social (batch B) // =========================================================================================== /// [`crate::chat::apply_send_roll`] with the roller named by guid — `MSG_RANDOM_ROLL`. @@ -734,7 +734,7 @@ pub fn gw_del_ignore(ctx: &ReducerContext, actor_guid: u64, target_guid: u64) -> } // =========================================================================================== -// Group (#479 batch B) +// Group (batch B) // =========================================================================================== /// [`crate::group::invite_core`] with the inviter named by guid. @@ -757,7 +757,7 @@ pub fn gw_group_decline(ctx: &ReducerContext, actor_guid: u64) -> Result<(), Str crate::group::decline_invite_for(ctx, actor_guid) } -/// [`crate::trade::apply_initiate_trade`] — propose a Trade Session against `target_guid` (#120). +/// [`crate::trade::apply_initiate_trade`] — propose a Trade Session against `target_guid`. /// Refusals are protocol answers on the trade-event relay, never an `Err` here. #[reducer] pub fn gw_initiate_trade( @@ -788,7 +788,7 @@ pub fn gw_cancel_trade(ctx: &ReducerContext, actor_guid: u64) -> Result<(), Stri } /// [`crate::trade::apply_set_trade_item`] — offer the item in absolute inventory slot `inv_slot` -/// in window slot `trade_slot` (#121). The gateway already mapped the client's (bag, slot) pair. +/// in window slot `trade_slot`. The gateway already mapped the client's (bag, slot) pair. #[reducer] pub fn gw_set_trade_item( ctx: &ReducerContext, @@ -801,7 +801,7 @@ pub fn gw_set_trade_item( crate::trade::apply_set_trade_item(ctx, acting, trade_slot, inv_slot) } -/// [`crate::trade::apply_clear_trade_item`] (#121). +/// [`crate::trade::apply_clear_trade_item`]. #[reducer] pub fn gw_clear_trade_item( ctx: &ReducerContext, @@ -813,7 +813,7 @@ pub fn gw_clear_trade_item( crate::trade::apply_clear_trade_item(ctx, acting, trade_slot) } -/// [`crate::trade::apply_set_trade_gold`] — `copper` is the offered amount (#121). +/// [`crate::trade::apply_set_trade_gold`] — `copper` is the offered amount. #[reducer] pub fn gw_set_trade_gold(ctx: &ReducerContext, actor_guid: u64, copper: u32) -> Result<(), String> { require_operator(ctx)?; @@ -822,7 +822,7 @@ pub fn gw_set_trade_gold(ctx: &ReducerContext, actor_guid: u64, copper: u32) -> } /// [`crate::trade::apply_accept_trade`] — accept the current offer; dual-accept runs the Trade -/// Commit (#122). +/// Commit. #[reducer] pub fn gw_accept_trade(ctx: &ReducerContext, actor_guid: u64) -> Result<(), String> { require_operator(ctx)?; @@ -830,7 +830,7 @@ pub fn gw_accept_trade(ctx: &ReducerContext, actor_guid: u64) -> Result<(), Stri crate::trade::apply_accept_trade(ctx, acting) } -/// [`crate::trade::apply_unaccept_trade`] — withdraw an accept (#122). +/// [`crate::trade::apply_unaccept_trade`] — withdraw an accept. #[reducer] pub fn gw_unaccept_trade(ctx: &ReducerContext, actor_guid: u64) -> Result<(), String> { require_operator(ctx)?; @@ -838,7 +838,7 @@ pub fn gw_unaccept_trade(ctx: &ReducerContext, actor_guid: u64) -> Result<(), St crate::trade::apply_unaccept_trade(ctx, acting) } -/// [`crate::trade::apply_busy_trade`] — decline a proposal as busy (#123). +/// [`crate::trade::apply_busy_trade`] — decline a proposal as busy. #[reducer] pub fn gw_busy_trade(ctx: &ReducerContext, actor_guid: u64) -> Result<(), String> { require_operator(ctx)?; @@ -846,7 +846,7 @@ pub fn gw_busy_trade(ctx: &ReducerContext, actor_guid: u64) -> Result<(), String crate::trade::apply_busy_trade(ctx, acting) } -/// [`crate::trade::apply_ignore_trade`] — decline a proposal via ignore (#123). +/// [`crate::trade::apply_ignore_trade`] — decline a proposal via ignore. #[reducer] pub fn gw_ignore_trade(ctx: &ReducerContext, actor_guid: u64) -> Result<(), String> { require_operator(ctx)?; @@ -925,7 +925,7 @@ pub fn gw_push_quest_to_party( } // =========================================================================================== -// Inventory / vendor / professions (#479 batch B) +// Inventory / vendor / professions (batch B) // =========================================================================================== /// [`crate::items::apply_item_move`] with the owner named by guid. @@ -1043,7 +1043,7 @@ pub fn gw_bind_home(ctx: &ReducerContext, actor_guid: u64) -> Result<(), String> } // =========================================================================================== -// Loot (#479 batch B) +// Loot (batch B) // =========================================================================================== /// [`crate::professions::skin_corpse`] with the skinner named by guid. @@ -1083,7 +1083,7 @@ pub fn gw_loot_master_give( } // =========================================================================================== -// Spells (#479 batch B) +// Spells (batch B) // =========================================================================================== /// [`crate::spell::do_cast_spell`] with the caster named by guid — the SPELLBOOK-GATED player cast @@ -1118,7 +1118,7 @@ pub fn gw_cancel_aura(ctx: &ReducerContext, actor_guid: u64, spell_id: u32) -> R } // =========================================================================================== -// World / misc (#479 batch B) +// World / misc (batch B) // =========================================================================================== /// [`crate::world::apply_gossip_select`] with the clicker named by guid — the notify-only hook. @@ -1153,7 +1153,7 @@ pub fn gw_learn_talent(ctx: &ReducerContext, actor_guid: u64, talent_id: u32) -> } /// [`crate::talent::do_reset_talents`] behind the gateway gate — the "I wish to unlearn my -/// talents." gossip option (work-item 198's respec primitive, wired to gossip by #516). +/// talents." gossip option (work-item 198's respec primitive, wired to gossip). #[reducer] pub fn gw_reset_talents( ctx: &ReducerContext, @@ -1256,7 +1256,7 @@ pub fn gw_pet_command( -/// [`crate::bridge::apply_client_command`] with the sender named by guid (#479, operator call: +/// [`crate::bridge::apply_client_command`] with the sender named by guid (operator call: /// the GM/bridge surface rides the gateway path like every other verb; authorization was never /// the connection identity). #[reducer] diff --git a/module/src/helpers.rs b/module/src/helpers.rs index e7b697f66..480a407cc 100644 --- a/module/src/helpers.rs +++ b/module/src/helpers.rs @@ -26,7 +26,7 @@ pub fn require_operator(ctx: &ReducerContext) -> Result<(), String> { /// /// THE actor chokepoint: every player-fired reducer resolves "who is acting" through here /// (60+ call sites, all shaped `entity_by_owner(ctx, ctx.sender()).ok_or_else(|| "... not in -/// world")?`). That is why the escrowed-transfer in-transit gate (issue #16) lives here rather than +/// world")?`). That is why the escrowed-transfer in-transit gate lives here rather than /// at each caller — an in-transit character reads as "not in world" everywhere, for free. /// `begin_transfer` also DELETES the live entity row in the same transaction it writes the escrow, /// so the target side (the ~50 `map_id`/`instance_id` gates, aggro candidate scans, threat lists, @@ -34,14 +34,14 @@ pub fn require_operator(ctx: &ReducerContext) -> Result<(), String> { /// braces: it fences the actor side even if a live row somehow survives. /// /// SCOPE: this gates the ACTOR side only. Reducers that reach a character by `character_guid` or by -/// name go through [`character_by_guid`] / [`character_by_name`] instead (issue #30) — see the +/// name go through [`character_by_guid`] / [`character_by_name`] instead — see the /// verdict table in `transfer.rs`'s module doc for the paths that are deliberately NOT gated. /// /// The candidate is collapsed to AT MOST one row with `.next()` FIRST, then handed to /// [`gate_by_guid`] — never `Iterator::filter` ahead of `.next()`, which would SKIP an in-transit /// row and hand back the next matching entity instead of none, silently defeating the fence rather /// than closing it. The sense of the check itself (in-transit ⇒ refuse) lives in -/// [`gate_in_transit`], not here — see its doc for why (issue #64). +/// [`gate_in_transit`], not here — see its doc for why. pub fn entity_by_owner(ctx: &ReducerContext, owner: Identity) -> Option { gate_by_guid( ctx, @@ -51,7 +51,7 @@ pub fn entity_by_owner(ctx: &ReducerContext, owner: Identity) -> Option Option Option Option /// The ONE place in the tree that asks "is this row's character mid-transfer?" and acts on the /// answer. All three chokepoints above route through it; `guid_of` is the only thing they differ in. /// -/// It exists because of the shape of the mutation it is defending against (#64/#380). Each +/// It exists because of the shape of the mutation it is defending against. Each /// chokepoint used to compute its own `in_transit` boolean inline, and a `!` glued anywhere into /// that expression — onto `is_in_transit`, or onto the `candidate.as_ref().is_some_and(..)` around /// it — inverts the fence with the same identifiers in the same order, which no `.contains()` scan @@ -125,7 +125,7 @@ fn gate_by_guid( /// The decision every in-transit fence in this file reduces to, pulled out pure and generic so its /// SENSE — not just its presence — can be pinned by a direct assertion instead of a source scan. /// -/// Issue #64: every chokepoint above used to spell this as `.filter(|x| !is_in_transit(..))` at +/// Every chokepoint above used to spell this as `.filter(|x| !is_in_transit(..))` at /// the call site. A scan can confirm `is_in_transit` is called and in what order, but nothing short /// of running the code can tell `!is_in_transit(..)` apart from `is_in_transit(..)` — same /// identifiers, same order, opposite meaning. Dropping that one `!` made `entity_by_owner` return @@ -152,25 +152,23 @@ pub(crate) fn dist_sq(a: &WorldEntity, b: &WorldEntity) -> f32 { dx * dx + dy * dy + dz * dz } -/// Max distance for a player-to-player interaction (a Trade Session, #119): (10 yd)², the same +/// Max distance for a player-to-player interaction (a Trade Session): (10 yd)², the same /// interaction-range convention as `VENDOR_RANGE_SQ` / `INSPECT_RANGE_SQ` et al. -#[allow(dead_code)] // callers land with the Trade Session work (#120) const PLAYER_INTERACTION_RANGE_SQ: f32 = 100.0; /// Why [`player_interaction_gate`] refused — a typed decision enum in the house pure-gate style /// (`trainer::BuyGrant`, `stacking::ApplyDecision`, `rolls::GroupLootDecision`): the Trade Session -/// opener (#120) maps each reason onto a distinct `SMSG_TRADE_STATUS` variant, a mapping that +/// opener maps each reason onto a distinct `SMSG_TRADE_STATUS` variant, a mapping that /// substring-matching `Err(String)` text would leave fragile. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[allow(dead_code)] // callers land with the Trade Session work (#120) pub(crate) enum PlayerInteractionDenied { ActorDead, NoTarget, TargetNotPlayer, TargetDead, /// The target is outside the actor's `(map_id, instance_id)` partition — either dimension. - /// "Partition", not "map": danger-zones §1.7 fixes the two-dimensional vocabulary, and #120 - /// must not read this as map-only when it picks a wire status. + /// "Partition", not "map": danger-zones §1.7 fixes the two-dimensional vocabulary, so the + /// Trade Session must not read this as map-only when it picks a wire status. DifferentPartition, OutOfRange, } @@ -180,7 +178,6 @@ pub(crate) enum PlayerInteractionDenied { /// The caller resolves the partner through a FENCED lookup ([`acting_entity_by_guid`]) and passes /// the result: a mid-transfer or offline partner arrives as `None` and reads NoTarget, the same /// absent-semantics the fence guarantees everywhere else. -#[allow(dead_code)] // callers land with the Trade Session work (#120) pub(crate) fn player_interaction_gate( actor: &WorldEntity, target: Option<&WorldEntity>, @@ -289,7 +286,7 @@ pub(crate) fn nearest_entity( /// so a ZERO-addressed row is visible to the owner-token coordinator alone, which is exactly who /// reads realm-core's events. Pure, so the fallback is pinned by a test rather than by a live node. /// -/// Moved here from `group.rs` (issue #371): work-item 187's roll/master-loot notifications +/// Moved here from `group.rs`: work-item 187's roll/master-loot notifications /// (`loot.rs`) and `chat.rs`'s whisper relay (`push_whisper`) both reuse this SAME address rule, so /// it lives with the other cross-module lookups rather than under the module it happened to be /// written for first. @@ -297,7 +294,7 @@ pub(crate) fn event_recipient_identity(bound: Option) -> Identity { bound.unwrap_or(Identity::ZERO) } -/// The live `game_world_entity` row for `guid`, the canonical "must be in world" fetch (issue #371) +/// The live `game_world_entity` row for `guid`, the canonical "must be in world" fetch /// that replaces the 38 hand-rolled `ctx.db.game_world_entity().guid().find(guid).ok_or_else(|| /// format!(...))?` call sites scattered across the reducer modules (debug.rs alone had 14). Most of /// those sites already spelled their error text exactly as this helper's default below; callers whose @@ -315,8 +312,8 @@ pub fn live_entity(ctx: &ReducerContext, guid: u64) -> Result` comparison) and precedence (a dead actor outranks an /// absent partner, so the client hears YouDead, not NoTarget). diff --git a/module/src/instance.rs b/module/src/instance.rs index 54313de88..b4dc20e58 100644 --- a/module/src/instance.rs +++ b/module/src/instance.rs @@ -10,7 +10,7 @@ //! instance you entered until it resets/reaps); dropped when the instance is reaped. //! - **Entry** — `resolve_or_create_instance`, called from the 225 areatrigger hook //! (`quest::apply_enter_areatrigger`) when the portal targets a DUNGEON map. Resolve order: **the -//! party's live instance → the character's own live binding → create** (issue #39 reversed the +//! party's live instance → the character's own live binding → create** (reversed the //! first two — the 190 design's binding-first order split a party whose members had entered //! separately). Solo entry allowed (binds to the character, `party_id = 0`), and a solo instance //! is ADOPTED by the party its holder has since joined so the members behind them join it too. @@ -19,7 +19,7 @@ //! the new instance id — templates/spawn rows are NEVER cloned), per-instance COPIES of the //! dungeon's interactive gameobjects (DOOR/BUTTON/CHEST/GOOBER, type-gated) — unless //! `game_config.hosts_instances` is off, in which case it files the row alone as a LEASE and the -//! shard that owns the map spawns the population via `ensure_instance` (issue #39). It does NOT arm a +//! shard that owns the map spawns the population via `ensure_instance`. It does NOT arm a //! dedicated 229 tick row — the catch-all covers every instance at the same cadence for free (perf //! catalog 1.3); `debug_arm_instance_tick` still arms one on demand for a faster cadence. //! - **Reap (slice 3)** — its own scheduled reducer (`reap_instances`, the `EventReaperSchedule` @@ -83,25 +83,25 @@ use crate::{ /// The maps that are DUNGEONS — entering an areatrigger portal targeting one of these /// resolves-or-creates a `game_instance` instead of landing at instance 0. /// -/// MOVED to `lyracore_shared::instance` by issue #48: the GATEWAY needs the same set to check, at +/// MOVED to `lyracore_shared::instance`: the GATEWAY needs the same set to check, at /// startup, that the database which will own a dungeon's instances actually hosts instance /// populations (`hosts_instances`) — and a set the two tiers could disagree about is exactly how -/// #48's empty dungeons stayed invisible. Re-exported here so every call site and every tripwire in +/// the empty dungeons stayed invisible. Re-exported here so every call site and every tripwire in /// this crate keeps naming it locally. Every entry MUST have an [`entrance_fallback`] arm /// (unit-pinned below) so a reaped-instance login can never strand. pub(crate) use lyracore_shared::instance::is_dungeon_map; /// The set itself has no non-test caller in this crate (`is_dungeon_map` is the read path); it is -/// imported under a distinct name — issue #376 reserves `DUNGEON_MAPS` locally for the +/// imported under a distinct name — the module reserves `DUNGEON_MAPS` locally for the /// dungeon-detail table below — for the cross-tier consistency pin, which is the invariant that /// keeps the shared set safe to extend from either tier. #[cfg(test)] pub(crate) use lyracore_shared::instance::DUNGEON_MAPS as SHARED_DUNGEON_MAP_IDS; -/// One fully-described dungeon (issue #376). This used to be FOUR hand-synchronized `match map_id` +/// One fully-described dungeon. This used to be FOUR hand-synchronized `match map_id` /// sites — [`entrance_fallback`] here, plus `world::graveyard::instance_release_zone` and /// `...instance_static_fallback` — each of which could independently gain (or omit) an arm for a /// map, and the omission surfaced only as a runtime `warn!` the first time a release actually hit -/// the gap (`resolve_graveyard`, pre-#376). A `DungeonMap` has no optional fields, so a map is now +/// the gap (`resolve_graveyard`, earlier). A `DungeonMap` has no optional fields, so a map is now /// either fully described here — entrance AND release zone AND release fallback, together — or /// absent, i.e. not a dungeon; there is no half-configured state left to warn about. pub(crate) struct DungeonMap { @@ -138,7 +138,7 @@ pub(crate) struct DungeonMap { /// Release zone 1581 (The Deadmines, cmangos AreaTable — CONFIRM against your own dump's /// `game_graveyard_zone` rows; a wrong id here just means the zone-linked lookup resolves nothing /// and the static fallback below applies) and release fallback Sentinel Hill are the same `[V]` -/// estimates the pre-#376 per-site consts always carried. +/// estimates the earlier per-site consts always carried. pub(crate) const DUNGEON_MAPS: &[DungeonMap] = &[DungeonMap { map_id: 36, entrance: (0, -11080.0, 1520.0, 46.0, 0.0), // [V] Moonbrook, Westfall @@ -168,17 +168,17 @@ pub(crate) const INSTANCE_EMPTY_REAP_MICROS: i64 = 30 * 60 * 1_000_000; /// doc). 60s: fine-grained enough that `reset_requested` lands fast, coarse enough to cost nothing. pub(crate) const INSTANCE_REAPER_INTERVAL_MICROS: i64 = 60 * 1_000_000; -/// How long a **LEASE** must read empty before the reaper takes it (issue #21). +/// How long a **LEASE** must read empty before the reaper takes it. /// /// A lease is a `game_instance` row on a database that does not host instance populations -/// (`game_config.hosts_instances = false`, issue #39) — the world shard of a Phase A deployment. +/// (`game_config.hosts_instances = false`) — the world shard of a Phase A deployment. /// It reads EMPTY within seconds of the last party member transferring to the instance shard, i.e. /// seconds after the run *starts*, because occupancy is counted from live player entities and there /// are none here any more. Reaping it on the ordinary 30-minute timer therefore deletes the world /// shard's only record of "this party is in instance N" **while they are still fighting in it**, and /// the next member through the portal — someone who died, released, and ran back — resolves /// `InstanceRoute::Create` and is teleported into a fresh, empty dungeon while their party is in the -/// old one. That is the same split #39 fixed from the other direction. +/// old one. That is the same split fixed from the other direction. /// /// **The countdown starts at the run's START, not at its end**, which is what sizes this number. /// The lease reads empty seconds after the party leaves the world shard, so the timer has to exceed @@ -196,9 +196,9 @@ pub(crate) const INSTANCE_REAPER_INTERVAL_MICROS: i64 = 60 * 1_000_000; /// /// Deliberate simplification: a constant, not a protocol — the simplest thing that closes the /// hole. Upgrade path is the one [`teardown_instance_inner`] already names: realm-core owns the -/// instance→shard index (#22) and the lease stops existing. **Single-database realms never reach +/// instance→shard index and the lease stops existing. **Single-database realms never reach /// this arm at all** — `hosts_instances` defaults to true, so their reap semantics are byte-for-byte -/// what they were (#21 AC#4). +/// what they were (AC#4). pub(crate) const INSTANCE_LEASE_REAP_MICROS: i64 = 12 * 60 * 60 * 1_000_000; /// The empty-timer this database reaps instances on: [`INSTANCE_EMPTY_REAP_MICROS`] where the @@ -294,7 +294,7 @@ crate::character_owned!(delete, fn sweep_delete_game_instance_binding(ctx, chara t.id().delete(b.id); } }); -// CROSS-DATABASE transport (issue #19): the binding is what "you re-enter YOUR instance" means, and +// CROSS-DATABASE transport: the binding is what "you re-enter YOUR instance" means, and // on the instance shard it is what `resolve_or_create_instance` reads when the character zones back // in after a disconnect. `instance_id` is carried VERBATIM — it is the id the gateway mirrored to // this shard via `ensure_instance`, so the two agree by construction. `id` is a surrogate PK. @@ -306,7 +306,7 @@ crate::character_owned!(transfer, fn sweep_transfer_game_instance_binding(ctx, c /// Drives the instance reaper (slice 3) — its OWN scheduled table per the `EventReaperSchedule` /// precedent (gc.rs untouched by design). Seeded by `seed::init` at 60s; a live (auto-migrated) -/// node re-arms via `debug_repair_after_publish` (#378, formerly the standalone +/// node re-arms via `debug_repair_after_publish` (formerly the standalone /// `debug_rearm_instance_reaper`) (init does not re-run on a plain publish — /// danger-zones "init only on fresh publish" rule). [server] #[table(accessor = game_instance_reaper_schedule, scheduled(reap_instances))] @@ -322,7 +322,7 @@ pub struct InstanceReaperSchedule { // =========================================================================================== /// The resolve order, as data: the party's live instance → the character's own live binding → -/// create (issue #39 swapped the first two; see [`route_instance`]). Pure — the DB reads live in +/// create (swapped the first two; see [`route_instance`]). Pure — the DB reads live in /// [`resolve_or_create_instance`]. #[derive(Debug, PartialEq)] pub(crate) enum InstanceRoute { @@ -336,7 +336,7 @@ pub(crate) fn route_instance( party_live: Option, ) -> InstanceRoute { match (own_binding_live, party_live) { - // THE PARTY OUTRANKS THE PERSONAL BINDING (issue #39 defect 3). This used to be the other + // THE PARTY OUTRANKS THE PERSONAL BINDING (defect 3). This used to be the other // way round, which SPLIT a party: a member who had already entered solo carried a binding // to their own `party_id = 0` instance, that binding won, and the rest of the party — who // could not see a solo instance through `by_party` — created a second dungeon. Vanilla's @@ -426,7 +426,8 @@ pub(crate) fn stranding_fallback( } } -#[allow(dead_code)] // core kept for a future gw_reset_instance twin (#483 deleted the sender-path reducer) +// Live only under `debug_reducers`: `apply_reset_instances` is its sole caller. +#[cfg_attr(not(feature = "debug_reducers"), allow(dead_code))] /// May this caller flag this instance for reset? Vanilla "Reset all instances" semantics: never /// while players are inside; a party instance takes its CURRENT leader, a solo instance /// (`party_id == 0`) takes any character bound to it (the caller reaches it via their own binding, @@ -473,7 +474,7 @@ fn bind_character(ctx: &ReducerContext, character_guid: u64, instance_id: u64, m } /// The dungeon-entry chokepoint (190 slice 2): resolve which instance of `target_map` this -/// character enters — party's live instance → own live binding → create (#39) — enforcing the 5-player +/// character enters — party's live instance → own live binding → create — enforcing the 5-player /// cap at trigger time. Solo entry allowed (binds to the character, `party_id = 0`). A stale /// binding (instance reaped or reset-flagged) self-heals: the row is dropped and resolution falls /// through. Returns the instance id to teleport into, or a loud `Err` (cap breach — surfaced by @@ -525,7 +526,7 @@ pub(crate) fn resolve_or_create_instance( let party_id = group.as_ref().map(|g| g.group_id).unwrap_or(0); match route_instance(own_live, party_live) { InstanceRoute::Own(id) => { - // ADOPTION (issue #39 defect 3, the other half of the split): the holder of a SOLO + // ADOPTION (defect 3, the other half of the split): the holder of a SOLO // instance who has since joined a party is the first of that party through the portal, // and `by_party` cannot see a `party_id = 0` instance — so without this the members // behind them would mint a second dungeon and the party would play in two of them. @@ -569,7 +570,7 @@ fn adopt_instance_for_party(ctx: &ReducerContext, instance_id: u64, party_id: u6 // create_instance — row + population + GO copies + tick row // =========================================================================================== -/// Does THIS database host dungeon-instance POPULATIONS (`game_config.hosts_instances`, issue #39)? +/// Does THIS database host dungeon-instance POPULATIONS (`game_config.hosts_instances`)? /// A missing config row reads `true`, so a database that predates the column — or a fresh one whose /// seed has not run — behaves exactly as it did before the flag existed. The `nav::nav_enabled` /// shape, deliberately: a world policy read at the one decision point, not a shard id. @@ -596,7 +597,7 @@ pub(crate) fn create_instance( /// [`create_instance`] with the id supplied by the caller (`0` = let `auto_inc` allocate, the /// normal path). The explicit form exists for ONE caller: [`ensure_instance`], the gateway's -/// cross-database mirror (issue #19), which has to re-create the SAME instance id on the shard that +/// cross-database mirror, which has to re-create the SAME instance id on the shard that /// owns the map so every party member's `game_instance_binding` — which travels in their transfer /// blob carrying that id verbatim — still resolves. pub(crate) fn create_instance_with_id( @@ -615,7 +616,7 @@ pub(crate) fn create_instance_with_id( }); let instance_id = inst.instance_id; - // --- THE HOSTING GATE (issue #39 defect 1). ------------------------------------------------ + // --- THE HOSTING GATE (defect 1). ------------------------------------------------ // A database that does not host instance populations files the row + (the caller's) binding and // spawns NOTHING: a LEASE, the exact shape `teardown_instance_inner(delete_row = false)` leaves // behind after a cross-database eviction. On the open-world shard of a Phase A deployment this @@ -739,7 +740,7 @@ pub(crate) fn create_instance_with_id( grid_x: lyracore_shared::spatial::grid_cell(src.x, src.y).0, grid_y: lyracore_shared::spatial::grid_cell(src.x, src.y).1, cell: lyracore_shared::spatial::cell_id_at(src.x, src.y), - // Copy the source door/chest/goober's real spawn quaternion (#515) — a dungeon copy is + // Copy the source door/chest/goober's real spawn quaternion — a dungeon copy is // the same prop at the same orientation, not a reset to identity. rotation_0: src.rotation_0, rotation_1: src.rotation_1, @@ -773,7 +774,7 @@ pub(crate) fn create_instance_with_id( // =========================================================================================== // =========================================================================================== -// Cross-database instance placement (issue #19) — the two calls the gateway drives +// Cross-database instance placement — the two calls the gateway drives // =========================================================================================== /// **Destination side.** Mirror instance `instance_id` of `map_id` onto THIS database, spawning its @@ -782,7 +783,7 @@ pub(crate) fn create_instance_with_id( /// /// The id is supplied, not allocated, because it was allocated on the SOURCE shard — the /// areatrigger hook runs where the player was standing, and the module deliberately knows nothing -/// about shards (spec #12: "no module game logic ever reads a shard id"). Every member's +/// about shards (spec "no module game logic ever reads a shard id"). Every member's /// `game_instance_binding` carries that id verbatim in their transfer blob, so mirroring it is what /// makes the party land in one instance. /// @@ -817,7 +818,7 @@ pub fn ensure_instance( /// **Source side.** Evict an instance's POPULATION from this database, keeping the `game_instance` /// row and its bindings as a lease (see [`teardown_instance_inner`]). /// -/// This is what makes issue #19 AC#2 true: the world shard spawned the dungeon when the first +/// This is what makes AC#2 true: the world shard spawned the dungeon when the first /// player stepped through the portal, but the run happens on the instance shard, so the world /// writer must stop ticking its creatures. Refuses while a live player is inside — the same guard /// `teardown_instance` has, and here it is load-bearing: a member who has NOT transferred yet is a @@ -848,7 +849,7 @@ pub fn evict_instance_population(ctx: &ReducerContext, instance_id: u64) -> Resu /// player-occupancy, then each instance stamps/clears/waits/reaps per [`occupancy_action`]. /// Teardown is batched ([`REAP_MAX_PER_FIRING`]); leftovers reap next firing (60s later). /// -/// **Reaper LOCALITY (#21 AC#1).** Every database runs its own reaper over its own +/// **Reaper LOCALITY (AC#1).** Every database runs its own reaper over its own /// `game_instance` rows and nothing else — there is no cross-database sweep and no shard id /// anywhere in it. A database that owns no instances therefore does no work at all: the row scan /// below finds nothing and returns *before* the O(entities) occupancy pass, so an open-world writer @@ -901,7 +902,7 @@ pub fn reap_instances(ctx: &ReducerContext, _schedule: InstanceReaperSchedule) { /// The set of instance ids with at least one live PLAYER entity — one pass classifies every /// instance at once (playerbots count: a parked bot holds its instance open, correctly). /// -/// Plus every instance CLAIMED by an in-transit character (issue #30, REFUSE verdict). Occupancy is +/// Plus every instance CLAIMED by an in-transit character (REFUSE verdict). Occupancy is /// counted from live entities, and `begin_transfer` deletes the live entity — so an instance whose /// only occupant is mid-transfer would read as empty and get torn down, deleting its /// `game_instance_binding` rows (a manifest table) out from under a transfer another shard is still @@ -928,7 +929,7 @@ pub(crate) fn teardown_instance(ctx: &ReducerContext, instance_id: u64) { teardown_instance_inner(ctx, instance_id, true) } -/// [`teardown_instance`], optionally KEEPING the `game_instance` row and its bindings (issue #19). +/// [`teardown_instance`], optionally KEEPING the `game_instance` row and its bindings. /// /// `keep_lease = true` is the cross-database eviction: the world shard created this instance when /// the first player stepped through the portal (the areatrigger hook runs there, and the module @@ -939,10 +940,10 @@ pub(crate) fn teardown_instance(ctx: &ReducerContext, instance_id: u64) { /// /// Note: the leased row is a stub the world shard never populates again, and it is left to the /// reaper's empty timer — but NOT the 30-minute one. A lease reads empty seconds after the party -/// transfers out, so a lease-only database reaps on [`INSTANCE_LEASE_REAP_MICROS`] instead (#21); +/// transfers out, so a lease-only database reaps on [`INSTANCE_LEASE_REAP_MICROS`] instead; /// the 30-minute run timer would delete the world shard's record of "this party is in instance N" /// while they were still in it. The cost is one row per run for the length of that timer. Upgrade -/// path: the instance→shard index moves to realm-core (#22) and neither the lease nor this arm +/// path: the instance→shard index moves to realm-core and neither the lease nor this arm /// exists. pub(crate) fn teardown_instance_inner(ctx: &ReducerContext, instance_id: u64, delete_row: bool) { if instance_id == 0 { @@ -958,9 +959,9 @@ pub(crate) fn teardown_instance_inner(ctx: &ReducerContext, instance_id: u64, de } // 1. Population: every non-player entity in the instance (alive or corpse), through the ONE - // canonical creature-teardown checklist (issue #359 — this loop, `encounter::despawn_tracked` + // canonical creature-teardown checklist (this loop, `encounter::despawn_tracked` // and `creatures::tick::pass_decay` had each grown their own copy, and they had diverged on - // the loot family, the taunt lock and the #50 withheld gate). + // the loot family, the taunt lock and the withheld gate). let doomed: Vec = entities .iter() .filter(|e| e.instance_id == instance_id && !e.is_player()) @@ -1017,7 +1018,7 @@ pub(crate) fn teardown_instance_inner(ctx: &ReducerContext, instance_id: u64, de // row (within one reducer transaction the order is atomic anyway; keeping it makes the // invariant readable: population never outlives its row). // - // SKIPPED for a cross-database eviction (`delete_row == false`, issue #19): the row is a + // SKIPPED for a cross-database eviction (`delete_row == false`): the row is a // LEASE the world shard keeps so the party's later arrivals resolve to the same instance, // and the bindings are the manifest rows that just travelled to the instance shard in each // member's transfer blob — deleting them here would delete the source copy of state the @@ -1052,8 +1053,9 @@ pub(crate) fn teardown_instance_inner(ctx: &ReducerContext, instance_id: u64, de // reset_instance — the party-leader / solo reset verb (slice 3 item 8) // =========================================================================================== -#[allow(dead_code)] // core kept for a future gw_reset_instance twin (#483 deleted the sender-path reducer) -/// The [`reset_instance`] body keyed off an explicit guid. Walks the caller's OWN bindings (being +// Live only under `debug_reducers`: `debug_reset_instance` is its sole caller. +#[cfg_attr(not(feature = "debug_reducers"), allow(dead_code))] +/// The instance-reset core keyed off an explicit guid. Walks the caller's OWN bindings (being /// bound is the reach — vanilla resets the instances you're bound to), gating each per /// [`reset_eligible`]. Returns how many instances were flagged, `Err` if none were eligible. pub(crate) fn apply_reset_instances( @@ -1120,7 +1122,7 @@ mod tests { // entrance to fall back to (the HearthstoneHome branch is the never-strand net for a // violation of exactly this pin). Checked against the SHARED (cross-tier) id set, not the // module-local DUNGEON_MAPS table — see `every_shared_dungeon_map_has_a_dungeon_maps_record` - // for why that distinction is the actual tripwire (issue #376). + // for why that distinction is the actual tripwire. for &m in SHARED_DUNGEON_MAP_IDS { assert!( entrance_fallback(m).is_some(), @@ -1135,12 +1137,12 @@ mod tests { ); } - /// The cross-tier tripwire (issue #376): [`lyracore_shared::instance::DUNGEON_MAPS`] is the id - /// list `gateway::config::ShardMap::check_instance_hosting` walks (issue #48) — every id in it + /// The cross-tier tripwire: [`lyracore_shared::instance::DUNGEON_MAPS`] is the id + /// list `gateway::config::ShardMap::check_instance_hosting` walks — every id in it /// MUST have a full record in this crate's [`DUNGEON_MAPS`], or a login/release on that map /// would silently take the "not a dungeon" path every helper here takes for an absent /// `DungeonMap`. This is the test-time replacement for the runtime `warn!` `resolve_graveyard` - /// used to log the first time a release actually hit a half-configured map (deleted by #376 — a + /// used to log the first time a release actually hit a half-configured map (deleted — a /// `DungeonMap` has no optional fields, so once an id clears this pin it can never regress into /// a half-configured state). #[test] @@ -1165,7 +1167,7 @@ mod tests { #[test] fn route_prefers_the_party_then_the_personal_binding_then_create() { - // Issue #39 defect 3 — THE SPLIT. Ginger entered Deadmines solo (instance 4, `party_id 0`), + // Defect 3 — THE SPLIT. Ginger entered Deadmines solo (instance 4, `party_id 0`), // then partied up; the party's own entry made instance 5. With the personal binding ranked // first she re-entered 4 while her party was in 5 — two dungeons, one party. The party now // wins, which is also vanilla's rule for a non-saved 5-man. @@ -1177,7 +1179,7 @@ mod tests { assert_eq!(route_instance(None, None), InstanceRoute::Create); } - /// #39 defect 3, the half a pure function cannot express: the FIRST party member through the + /// Defect 3, the half a pure function cannot express: the FIRST party member through the /// portal may be the one holding the solo binding, and `by_party` cannot see a `party_id = 0` /// instance — so the members behind them would mint a second dungeon unless the instance they /// walk into is re-stamped as the party's. Source-scanned (the `transfer.rs` tripwire pattern): @@ -1230,7 +1232,7 @@ mod tests { ); } - /// #39 defect 1: entering a portal for a map another database owns must never spawn the + /// Defect 1: entering a portal for a map another database owns must never spawn the /// dungeon HERE. The spawn loop is a `ReducerContext` walk with no unit harness, so the gate in /// front of it is source-scanned — the mutation that matters (deleting the early return) leaves /// every behavioural test in the workspace green. @@ -1267,7 +1269,7 @@ mod tests { source scan above still passes" ); // The reader itself must default to hosting, so a database with no config row (or one that - // predates the column) behaves exactly as it did before #39. + // predates the column) behaves exactly as it did. let reader = code_of( include_str!("instance.rs"), "pub(crate) fn hosts_instance_populations(", @@ -1313,7 +1315,7 @@ mod tests { `false` default would switch dungeon spawning off on every existing database the moment \ it auto-migrates" ); - // #377: `init` is a 4-line dispatcher over four banner-stratum fns now (see seed.rs's + // `init` is a 4-line dispatcher over four banner-stratum fns now (see seed.rs's // header) — `game_config` is seeded in stratum 1, `seed_production_core`. let seed = code_of( include_str!("seed.rs"), @@ -1327,7 +1329,7 @@ mod tests { } /// Isolate one fn body and strip its `//` prose, so a tripwire asserts on CODE and not on the - /// comment that explains it. Shared as [`crate::test_scan::code_of`] (issue #64 — this used to + /// comment that explains it. Shared as [`crate::test_scan::code_of`] (this used to /// be six near-identical, drifted-apart copies). use crate::test_scan::code_of; @@ -1388,7 +1390,7 @@ mod tests { ); } - /// **#21 AC#4** — reap semantics on a shard-pool deployment, stated against the single-database + /// **AC#4** — reap semantics on a shard-pool deployment, stated against the single-database /// ones they must not change. #[test] fn a_lease_only_database_holds_its_stub_rows_far_past_the_thirty_minute_run_timer() { @@ -1462,9 +1464,9 @@ mod tests { } } - /// **#21 AC#1**, the half that is a cost statement rather than a behaviour: the reaper on a + /// **AC#1**, the half that is a cost statement rather than a behaviour: the reaper on a /// database with no instances must not pay for the O(entities) occupancy pass. Source-scanned - /// like the #39 gate above — the reducer body has no unit harness, and moving the early return + /// like the gate above — the reducer body has no unit harness, and moving the early return /// below the scan leaves every behavioural test in the workspace green while the open-world /// writer starts paying, once a minute, for a table it has no rows in. #[test] @@ -1497,7 +1499,7 @@ mod tests { ); } - /// **#21 AC#1**, per-instance schedules. Nothing that ticks an instance may be armed on a + /// **AC#1**, per-instance schedules. Nothing that ticks an instance may be armed on a /// database that does not host its population: the lease path must file the row and stop. #[test] fn a_lease_arms_no_per_instance_schedule_and_spawns_no_population() { diff --git a/module/src/items/economy.rs b/module/src/items/economy.rs index 369ed7a3d..18736dec9 100644 --- a/module/src/items/economy.rs +++ b/module/src/items/economy.rs @@ -1,5 +1,5 @@ //! The vendor/armorer economy — sell / buyback / buy / repair, plus the shared NPC-interaction trust -//! gate all four reduce to (#387: split off `ops.rs`, pure code-motion, no behavior change). Each +//! gate all four reduce to (split off `ops.rs`, pure code-motion, no behavior change). Each //! `apply_*` is the shared core behind a thin player reducer and its debug twin (see `reducers.rs`). use spacetimedb::{ReducerContext, Table}; @@ -23,7 +23,7 @@ const VENDOR_RANGE_SQ: f32 = 100.0; /// The shared trust-boundary gate for a player-initiated NPC interaction — vendor sell/buyback/buy and /// armorer repair all reduce to the SAME five checks: the player must be alive, the target must be a /// real NPC (never another player, `is_player()`) carrying `required_flag`, on the player's own -/// map+instance, within `VENDOR_RANGE_SQ`. Extracted (issue #372, "the exact place drift happens" — the +/// map+instance, within `VENDOR_RANGE_SQ`. Extracted ("the exact place drift happens" — the /// 190 review already caught one copy of this shape missing the instance check once) from /// `apply_item_sell` / `apply_buyback_item` / `apply_buy_item` / `apply_player_repair`, which used to /// paste this ~20-line block four times. @@ -263,7 +263,7 @@ pub(crate) fn apply_item_sell( vendor_guid: u64, slot: u8, ) -> Result<(), String> { - // Vendor gating, mirroring apply_buy_item/apply_buyback_item/apply_player_repair (issue #372's + // Vendor gating, mirroring apply_buy_item/apply_buyback_item/apply_player_repair (the // shared npc_interaction_gate): a dead/ghost player can't vendor; the sale must be to a real // VENDOR creature within range on the same map+instance. Unlike buy there is NO vendor_sells() // check — vanilla lets you sell any sellable item to any vendor (junk goes to anyone), only the @@ -403,7 +403,7 @@ pub(crate) fn apply_buy_item( // standing at, and that vendor must actually stock the item — you can't buy arbitrary items or // from a non-vendor. // - // ORDERING NOTE (issue #372): before this extraction, `count == 0` was checked BETWEEN the dead + // ORDERING NOTE: before this extraction, `count == 0` was checked BETWEEN the dead // check and the vendor-resolution gate; `npc_interaction_gate` folds dead+vendor into one atomic // check, so `count == 0` now runs AFTER the vendor/range gate instead. This only changes which // error string comes back for a MALFORMED packet that combines count=0 with a bogus/out-of-range @@ -449,7 +449,7 @@ pub(crate) fn apply_buy_item( } // DEBIT FIRST: deduct (saturating, never wraps) and persist the player BEFORE granting the item, so a // subsequent failure (inventory full) rolls the whole tx back rather than charging for nothing. A buy - // of more than `max_stack` now SPILLS across stacks (parity #14) — and tops up an existing partial + // of more than `max_stack` now SPILLS across stacks (parity) — and tops up an existing partial // stack of the same item — instead of being rejected. player.money = player.money.saturating_sub(cost); let owner_identity = player.owner_identity; @@ -503,7 +503,7 @@ pub(crate) fn apply_player_repair( slot: u8, ) -> Result<(), String> { // REPAIR-NPC gate — same shape as apply_item_sell's vendor gate, keyed on the REPAIR flag - // (issue #372's shared npc_interaction_gate). + // (the shared npc_interaction_gate). let (mut player, _npc) = npc_interaction_gate( ctx, player_guid, @@ -696,7 +696,7 @@ mod tests { } } - /// #514: this crate has no `ReducerContext` harness by design (`test_scan`'s doc comment / + /// This crate has no `ReducerContext` harness by design (`test_scan`'s doc comment / /// playbook §7), so `apply_player_repair`'s actual gating/cost/durability-restore behavior /// against real table state is verified live via the wire harness, not here — the same boundary /// every other reducer in this module lives behind (see `combat/swing.rs`'s chokepoint tests for diff --git a/module/src/items/inventory.rs b/module/src/items/inventory.rs index dd6ffca2a..d7e8d2065 100644 --- a/module/src/items/inventory.rs +++ b/module/src/items/inventory.rs @@ -1,4 +1,4 @@ -//! Move / split / equip / unequip + the slot-space vocabulary and free-slot search (#387: split off +//! Move / split / equip / unequip + the slot-space vocabulary and free-slot search (split off //! `ops.rs`, pure code-motion, no behavior change except the free-slot search's index-scan count — see //! `first_free_backpack_slot`/`first_free_bag_slot`). Each `apply_*` is the shared core behind a thin //! player reducer and its debug twin (see `reducers.rs`). @@ -18,17 +18,19 @@ use super::tables::{ }; use crate::{game_player_reputation, game_player_skill}; -#[allow(dead_code)] // core kept for a future gw_split_item twin (#483 deleted the sender-path reducer) -/// Shared stack-split logic for the player + debug paths: split `count` units off the stack in `slot` -/// into the empty `to_slot`, leaving the remainder in the source. Vanilla only splits a STRICT subset +// Live only under `debug_reducers`: `debug_split_item` is its sole caller. +#[cfg_attr(not(feature = "debug_reducers"), allow(dead_code))] +/// Stack-split core: split `count` units off the stack in `slot` into the empty `to_slot`, leaving +/// the remainder in the source. Vanilla only splits a STRICT subset /// (you can't split off the whole stack — that's a move), so `count == 0` or `count >= stack_count` is -/// rejected; the destination must be empty AND outside the equipment region (`valid_split_dest_slot`, -/// #360) — a split can never legitimately land on the body, and unlike `apply_item_move` this path runs -/// no equip-validation at all, so admitting 0..=18 here bypassed `can_equip_into`/proficiency/ -/// required-level/BoE entirely. The new partial-stack row reuses the source's entry / owner / -/// durability and takes a fresh per-slot guid (`item_guid_for`) + the current timestamp. Errors if the -/// source slot is empty, the count is invalid, the destination is an equipment slot, or the destination -/// is occupied. Additive — decrements the source row and inserts one new item row. [entity] +/// rejected; the destination must be empty AND outside the equipment region +/// (`valid_split_dest_slot`) — a split can never legitimately land on the body, and unlike +/// `apply_item_move` this path runs no equip-validation at all, so admitting 0..=18 here bypassed +/// `can_equip_into`/proficiency/required-level/BoE entirely. The new partial-stack row reuses the +/// source's entry / owner / durability and takes a fresh per-slot guid (`item_guid_for`) + the +/// current timestamp. Errors if the source slot is empty, the count is invalid, the destination is +/// an equipment slot, or the destination is occupied. Additive — decrements the source row and +/// inserts one new item row. [entity] pub(crate) fn apply_item_split( ctx: &ReducerContext, player_guid: u64, @@ -382,7 +384,8 @@ pub(crate) fn is_carried_slot(slot: u8) -> bool { || (BAG_CONTENT_OFFSET..BAG_CONTENT_END).contains(&slot) // 120..=191 } -#[allow(dead_code)] // core kept for a future gw_split_item twin (#483 deleted the sender-path reducer) +// Live only under `debug_reducers`: `apply_item_split` is its sole caller. +#[cfg_attr(not(feature = "debug_reducers"), allow(dead_code))] /// The destination-slot gate for `apply_item_split` ONLY: everything `valid_dest_slot` admits, MINUS /// the equipment region (0..=`equip_slot::END`, i.e. 0..=18). A split can never legitimately place an /// item on the body — `apply_item_move` is the only path that runs equip-validation @@ -403,7 +406,7 @@ pub(crate) fn bag_content_decompose(to_slot: u8) -> (u8, u8) { (offset / MAX_BAG_SIZE, offset % MAX_BAG_SIZE) } -/// The bag-content CAPACITY gate shared by `apply_item_move` and `apply_item_split` (issue #372): once +/// The bag-content CAPACITY gate shared by `apply_item_move` and `apply_item_split`: once /// `to_slot` has already passed `valid_dest_slot`/`valid_split_dest_slot`, a bag-content destination /// (120..=191) additionally needs the corresponding bag-equip slot to actually hold an equipped bag /// whose template covers `slot_in_bag`. A no-op (`Ok(())`) for a non-bag-content destination — the @@ -439,7 +442,8 @@ pub(crate) fn validate_bag_dest_slot( Ok(()) } -#[allow(dead_code)] // core kept for a future gw_split_item twin (#483 deleted the sender-path reducer) +// Live only under `debug_reducers`: `apply_item_split` is its sole caller. +#[cfg_attr(not(feature = "debug_reducers"), allow(dead_code))] /// A split must leave at least one unit in BOTH the source and the new stack — splitting off none /// (`count == 0`) or the whole stack (`count >= stack_count`, that's a move) is rejected. Extracted from /// `apply_item_split` (pure code-motion) so the count-gate boundaries are unit-tested without a live @@ -449,7 +453,7 @@ pub(crate) fn valid_split_count(count: u32, stack_count: u32) -> bool { } /// First backpack slot (23..39) not occupied by any of the player's items, or `None` if the backpack -/// is full. Collects the owner's occupied slots into a set ONCE (#387 smalls: was one `by_owner_guid` +/// is full. Collects the owner's occupied slots into a set ONCE (smalls: was one `by_owner_guid` /// index scan PER CANDIDATE slot — up to 16 full scans for a nearly-full backpack) then does a plain /// membership check per candidate. Vanilla auto-store fills the first free bag slot. /// The occupied-slot set behind the two backpack probes below — one spelling of the scan. @@ -510,7 +514,7 @@ fn first_free_bag_equip_slot(ctx: &ReducerContext, player_guid: u64) -> Option Option { let templates = ctx.db.game_item_template(); @@ -759,14 +763,14 @@ mod tests { assert!(!valid_dest_slot(255)); } - /// SPLIT DESTINATION GATE (#360 regression): a split can never legitimately target the body, so + /// SPLIT DESTINATION GATE (regression): a split can never legitimately target the body, so /// `valid_split_dest_slot` must refuse every equipment-region slot (0..=18) even though /// `valid_dest_slot` alone admits it (0..=38 is the modeled equip+bag-equip+backpack range). Every /// non-equipment slot `valid_dest_slot` admits stays admitted. #[test] fn valid_split_dest_slot_refuses_the_equipment_region() { // The whole equipment region (0..=18, e.g. HEAD=0..=TABARD=18) is valid for a plain move/dest - // check but MUST be refused for a split — this is the exact bypass #360 reports. + // check but MUST be refused for a split — this is the exact reported bypass. for slot in 0u8..=equip_slot::END { assert!( valid_dest_slot(slot), diff --git a/module/src/items/mod.rs b/module/src/items/mod.rs index 48aff3d6d..5af54c19a 100644 --- a/module/src/items/mod.rs +++ b/module/src/items/mod.rs @@ -13,10 +13,10 @@ //! This file is a thin facade over five by-concern submodules — `tables` (the two table structs + RLS //! filter + guid/slot primitives), `rules` (the pure equip/economy taxonomy + arithmetic + their unit //! tests), `ops` (the grant/use/loot mutation cores), `economy` (the vendor sell/buy/buyback/repair -//! cores + their shared NPC gate, #387), and `inventory` (the move/split/equip/unequip cores + slot-space -//! vocabulary, #387). The `pub use ...::*` re-exports keep every symbol reachable as +//! cores + their shared NPC gate), and `inventory` (the move/split/equip/unequip cores + slot-space +//! vocabulary). The `pub use ...::*` re-exports keep every symbol reachable as //! `crate::items::` regardless of which submodule defines it. The sender-path `#[reducer]` entry -//! points that used to live in a `reducers` submodule here are gone (#483) — the gateway's `gw_*` +//! points that used to live in a `reducers` submodule here are gone — the gateway's `gw_*` //! reducers (`gw.rs`) are the only surviving callers of these cores. mod economy; diff --git a/module/src/items/ops.rs b/module/src/items/ops.rs index 871a30f89..1bb39ee06 100644 --- a/module/src/items/ops.rs +++ b/module/src/items/ops.rs @@ -1,5 +1,5 @@ //! The `apply_*` mutation cores that don't belong to the vendor economy (`economy.rs`) or the -//! move/split/equip slot machinery (`inventory.rs`, #387 split): grant / use items, take corpse loot, +//! move/split/equip slot machinery (`inventory.rs`, split): grant / use items, take corpse loot, //! the starter-loadout grant, and the equipped-stat sum. Each is the shared core behind a thin player //! reducer and its debug twin (see `reducers.rs`). All effects are additive: they touch only the item //! rows + the actor's health/money. @@ -172,7 +172,7 @@ pub(crate) fn grant_item( ) } -/// Add `count` units of `tmpl` to `player_guid`'s backpack the way vanilla auto-store does (parity #14): +/// Add `count` units of `tmpl` to `player_guid`'s backpack the way vanilla auto-store does (parity): /// TOP UP existing partial stacks of the same entry first (lowest slot first), then spill the remainder /// into free backpack slots (each new stack ≤ `max_stack`). `Err("inventory full")` if it can't all fit — /// the caller's reducer rolls back (so a buy that overflows un-charges). Shared by grant / buy / loot so a @@ -371,7 +371,7 @@ pub(crate) fn remove_items( pub(crate) const RECENTLY_BANDAGED_SPELL: u32 = 11196; /// The on-use spell for `tmpl`, or `None` if it carries no on-use effect. `spellid_1`/`spelltrigger_1` -/// (`ItemTemplate`, shipped to the client for tooltips) are the SINGLE on-use authority (#387, finishing +/// (`ItemTemplate`, shipped to the client for tooltips) are the SINGLE on-use authority (finishing /// the migration `rules.rs` used to name as the end state): a nonzero `spellid_1` whose trigger slot is /// `ItemSpellTriggerType::OnUse` (0) IS the item's on-use cast. This retires the old hardcoded /// `USE_EFFECTS` shadow-map (a duplicate of exactly this column) — the importer/seed data populate the @@ -387,7 +387,7 @@ fn use_spell_for(tmpl: &ItemTemplate) -> Option { /// Two kinds qualify today, both keyed on the SPELL's effect kind rather than an item-entry allowlist, so /// any future recall trinket or mount is reusable with zero code: /// -/// - `E_RECALL_HOME` (0x1F) — the Hearthstone's shape (#387). A recall trinket is never used up. +/// - `E_RECALL_HOME` (0x1F) — the Hearthstone's shape. A recall trinket is never used up. /// - `A_MOUNTED` — a mount item, via `mount::spell_is_mount`, the one place that classification lives. /// Vanilla mounts are permanent bag items, not one-shot potions. /// @@ -443,7 +443,7 @@ fn spell_restores_power(ctx: &ReducerContext, spell_id: u32) -> bool { } /// Shared use-an-item logic for the player + debug paths: resolve the item -> run its gates -> consume -/// one unit (unless it's a non-consuming recall item) -> `begin_cast` its `spellid_1` (#387). `spellid_1` +/// one unit (unless it's a non-consuming recall item) -> `begin_cast` its `spellid_1`. `spellid_1` /// (trigger 0) is the SINGLE on-use authority — this retired the old three-mechanism `apply_item_use` /// (a hardcoded Hearthstone special case, the `USE_EFFECTS` shadow-map, and a legacy vital-restore /// fallback with its own duplicated mana gate): every usable item, consumable or not (a potion, a @@ -476,7 +476,7 @@ pub(crate) fn apply_item_use( .entry() .find(inst.entry) .ok_or_else(|| format!("no template for item entry {}", inst.entry))?; - // spellid_1 (trigger 0) is the item's on-use spell — the single authority now (#387). No mapped + // spellid_1 (trigger 0) is the item's on-use spell — the single authority now. No mapped // spell means nothing to "use" (a plain reagent, a piece of gear, an un-migrated item). let spell_id = use_spell_for(&tmpl).ok_or_else(|| format!("item {} is not consumable", inst.entry))?; @@ -495,7 +495,7 @@ pub(crate) fn apply_item_use( ) { return Err("Recently Bandaged".to_string()); } - // GATE A (the one surviving mana-class gate, #387 retired its legacy-branch duplicate): a mana potion + // GATE A (the one surviving mana-class gate, retired its legacy-branch duplicate): a mana potion // / drink restores POWER through E_ENERGIZE / A_PERIODIC_ENERGIZE — for a non-mana class that is // rage/energy, the out-of-combat preload exploit. `begin_cast` has no power-class gate of its own, so // DENY a power-restoring on-use spell here, BEFORE consuming/casting, when the user isn't a mana @@ -710,7 +710,7 @@ pub(crate) fn apply_take_loot( .entry() .find(row.item_entry) .ok_or_else(|| format!("no template for item entry {}", row.item_entry))?; - // Auto-store with stacking (parity #14): a looted stackable tops up a matching partial stack first, + // Auto-store with stacking (parity): a looted stackable tops up a matching partial stack first, // then spills into free slots (visible only to its owner via owner_identity). `?` so an inventory-full // loot rolls back and the loot row stays for a retry. store_item( @@ -771,7 +771,7 @@ mod tests { bandage_cooldown_blocks, death_durability_loss, use_spell_for, RECENTLY_BANDAGED_SPELL, }; - /// `use_spell_for` (#387): `spellid_1` is the on-use spell IFF it's nonzero AND `spelltrigger_1` names + /// `use_spell_for`: `spellid_1` is the on-use spell IFF it's nonzero AND `spelltrigger_1` names /// the on-use trigger slot (0 — `ItemSpellTriggerType::OnUse`). A trigger-1 (on-equip) spell is NOT /// an on-use cast even with a nonzero id, and a zero `spellid_1` (a plain reagent/gear piece/ /// un-migrated item) resolves to `None` regardless of its trigger byte. Replaces the old hardcoded diff --git a/module/src/items/rules.rs b/module/src/items/rules.rs index 5c7c760f9..43568806e 100644 --- a/module/src/items/rules.rs +++ b/module/src/items/rules.rs @@ -4,7 +4,7 @@ //! taxonomy/arithmetic tests live at the bottom of this file. //! //! `consumable_heal` — the flat-plus-item-level HP formula that stood in for a real on-use effect id — -//! is RETIRED (#387): every on-use item now carries a real `spellid_1` cast through `begin_cast` +//! is RETIRED: every on-use item now carries a real `spellid_1` cast through `begin_cast` //! (`items::ops::apply_item_use`), so the magnitude lives in `game_spell_effect.base_points` like any //! other spell, not in a second, item-only formula. @@ -603,7 +603,7 @@ pub(crate) mod tests { /// A bare `ItemTemplate` for the stat-sum tests: zero everything except the stat columns the test /// sets via the returned mutable handle. Keeps the pure stat arithmetic testable without a module. - // pub(crate): reused by `trade::tests` (#121) — the one 100-field template fixture. + // pub(crate): reused by `trade::tests` — the one 100-field template fixture. pub(crate) fn blank_template(entry: u32) -> ItemTemplate { ItemTemplate { entry, diff --git a/module/src/items/tables.rs b/module/src/items/tables.rs index bf066418c..147381914 100644 --- a/module/src/items/tables.rs +++ b/module/src/items/tables.rs @@ -243,7 +243,7 @@ crate::character_owned!(delete, fn sweep_delete_game_item_instance(ctx, characte items.guid().delete(r.guid); } }); -// CROSS-DATABASE transport (issue #19), HOT: worn gear + bags. The item `guid` is PRESERVED, unlike +// CROSS-DATABASE transport, HOT: worn gear + bags. The item `guid` is PRESERVED, unlike // every surrogate-PK table here: it is derived from the OWNER guid (`item_guid_for`), so it is // already namespaced per character and cannot collide with the destination's own items — and it is // the id the CLIENT knows an item by (equipment slots, loot, trade). Re-minting it would make every @@ -363,7 +363,7 @@ crate::character_owned!(delete, fn sweep_delete_game_character_buyback(ctx, char buybacks.id().delete(r.id); } }); -// CROSS-DATABASE transport (issue #19): the buyback ring is real value the player can still reclaim +// CROSS-DATABASE transport: the buyback ring is real value the player can still reclaim // (they paid for the item once). Dropping it at a shard boundary is a silent refund window closing. // `id` orders the ring (newest = highest), so re-minting preserves the order: `move_rows` inserts in // export order, and export order is index order. diff --git a/module/src/lib.rs b/module/src/lib.rs index 0bbcea245..3e7550bda 100644 --- a/module/src/lib.rs +++ b/module/src/lib.rs @@ -41,8 +41,8 @@ /// &ReducerContext, guid: u64, identity: Identity)`. /// /// `character_owned!(transfer, fn (, , ) { table = .., by = .., .. })` and -/// `character_owned!(not_transported, fn ())` — the CROSS-DATABASE row transport (issue -/// #19), which is what lets a table's rows actually leave one SpacetimeDB database and arrive in +/// `character_owned!(not_transported, fn ())` — the CROSS-DATABASE row transport, +/// which is what lets a table's rows actually leave one SpacetimeDB database and arrive in /// another. Both expand to `pub(crate) fn (ctx: &ReducerContext, guid: u64, io: &mut /// crate::transfer::RowIo)`. Required for every `game_*` manifest table — /// `transfer::every_manifest_table_can_cross_a_database_boundary` fails the test suite if one is @@ -52,11 +52,11 @@ /// /// See `crate::reputation::sweep_delete_game_player_reputation` for a worked example. /// -/// # The transport GRAMMAR, and why it is a grammar (#380) +/// # The transport GRAMMAR, and why it is a grammar /// /// A transport arm used to be a free-form `$body:block`, so "does this arm actually transport, or /// does it silently drop the table's rows?" was a question only a source SCANNER could answer — and -/// a scanner is an arms race it eventually loses. The #36 review proved it: repointing +/// a scanner is an arms race it eventually loses. The review proved it: repointing /// `sweep_transfer_game_item_instance` at `not_transported` deleted every character's gear on every /// hop with 468 module tests green, and each hardening of the scan (`contains("move_rows")`, then /// "exactly once", then "at the top", then "and it filters by the guid") was defeated by the next @@ -263,7 +263,7 @@ mod exploration; // clear_creatures) for the client-automation harness. PROD-SAFE: the whole module is behind the // `debug_reducers` Cargo feature (default OFF) — a plain build / production publish compiles it out // entirely. Enable for the test build via `--build-options='--features=debug_reducers'`. See -// debug/mod.rs (#386 split this into a directory along its section banners: mod/readout/audit/ +// debug/mod.rs (split this into a directory along its section banners: mod/readout/audit/ // repair/encounter/instance/fingerprint). #[cfg(feature = "debug_reducers")] mod debug; @@ -279,7 +279,7 @@ mod gm; // Consumption (the state-gated ray merge) is the `game_go_collider` registry. mod go_model; // Graveyard resolution (work-item 209/226): the death-release subsystem `world::do_repop` calls to -// pick where a ghost teleports. Extracted from `world.rs` (issue #385). +// pick where a ghost teleports. Extracted from `world.rs`. mod graveyard; mod group; mod gw; @@ -300,7 +300,7 @@ mod mail; /// caller-chosen id, and the reaper. The mechanism for moving value into a mail row across a /// database boundary no transaction spans; the single-database plane deliberately bypasses it. mod mail_escrow; -/// Batched movement republish (#461): the PRIVATE `game_entity_motion_pending` staging table that +/// Batched movement republish: the PRIVATE `game_entity_motion_pending` staging table that /// `movement_update` writes, and the 20 Hz `publish_motion` tick that drains it into the public /// `game_entity_motion` relay in one transaction. mod motion; @@ -318,7 +318,7 @@ mod package_config; /// `import_meta` is not: nothing outside this module reads its table. mod package_import; mod professions; -/// Deploy-safety tripwire (#223): source-scans `scripts/**` + `tools/**` for a destructive +/// Deploy-safety tripwire: source-scans `scripts/**` + `tools/**` for a destructive /// `spacetime publish -c`, and pins the sanctioned deploy script's own argv guard and required /// flags. Test-only; reads files, never runs the CLI. #[cfg(test)] @@ -349,11 +349,11 @@ mod threat; mod trade; mod trainer; mod transfer; -/// Exact per-cell vmap collision-triangle store + LoS/collision ray queries (issue #521, part of -/// the #169 full-vmap epic). Builds on `nav`'s obstruction-grid approximation with an exact +/// Exact per-cell vmap collision-triangle store + LoS/collision ray queries (part of +/// the full-vmap epic). Builds on `nav`'s obstruction-grid approximation with an exact /// triangle store; see the module doc comment for the split. pub mod vmap; -// Source-scan tripwires (issue #379 pulled these out of this file, which had grown to 1,146 lines — +// Source-scan tripwires (pulled these out of this file, which had grown to 1,146 lines — // four fifths of it cfg(test) scan machinery — so the doc comment atop this file ("this is the thin // index") stayed true). See `tripwires.rs`'s own module doc for the roster and the shared engine in // `test_scan.rs` it now runs on. @@ -369,7 +369,7 @@ pub use action_bar::*; pub use auction::*; pub use auth::*; pub use breath::*; -pub use breath_relay::BreathRelayEvent; // gateway schema-parity relay (#141) +pub use breath_relay::BreathRelayEvent; // gateway schema-parity relay pub use bridge::*; pub use character::*; pub use chat::*; @@ -382,7 +382,7 @@ pub use debug::*; pub use duel::*; pub use encounter::*; pub use exploration::CharacterExplored; // re-exported for the gateway schema-parity test (282) -pub use rest::RestStateEvent; // re-exported for the gateway schema-parity test (#468 4c) +pub use rest::RestStateEvent; // re-exported for the gateway schema-parity test (4c) pub use spell::stacking::SpellGroupRule; // Keeps this generated table in schema-parity coverage. pub use faction::*; pub use gameobject::*; @@ -411,7 +411,7 @@ pub use taxi::*; pub use threat::*; pub use trade::*; pub use trainer::*; -// Re-exported for the gateway schema-parity test (#19). +// Re-exported for the gateway schema-parity test. pub use transfer::{BotTransferIntent, TransferOut}; pub use weather::*; pub use world::*; diff --git a/module/src/load.rs b/module/src/load.rs index c1ca3a09d..6f53a1059 100644 --- a/module/src/load.rs +++ b/module/src/load.rs @@ -1,4 +1,4 @@ -//! Per-shard writer occupancy and per-region population, queryable as plain SQL (issue #78). +//! Per-shard writer occupancy and per-region population, queryable as plain SQL. //! //! The GATEWAY is the one component that can see the whole realm — every shard's `/v1/metrics` //! endpoint, every session, every live position — so it is the one that SAMPLES (a periodic @@ -9,7 +9,7 @@ //! shard id, so it has no idea which database it is running on). //! //! Two tables, RING-BUFFERED rather than TTL-reaped — each keeps the last [`SHARD_LOAD_RING`] / -//! [`REGION_LOAD_RING`] samples per key, oldest evicted on insert. Issue #211 is the reason this is +//! [`REGION_LOAD_RING`] samples per key, oldest evicted on insert. The reason this is //! spelled out: a `game_*` table nothing ever reaps grows forever, and a scheduled reaper is not the //! only way to avoid that — bounding the table AT WRITE TIME needs no schedule, no GC pass, and no //! extra reducer. At the gateway's default 30s sample cadence, 20 samples is ~10 minutes of @@ -49,7 +49,7 @@ pub struct ShardLoad { pub sampled_at_micros: i64, pub writer_occupancy_pct: f32, pub sessions: u32, - /// Which GATEWAY PROCESS wrote this sample (issue #308). With N gateway processes sampling the + /// Which GATEWAY PROCESS wrote this sample. With N gateway processes sampling the /// same shard, `sessions` is only ever that ONE process's own player-connection cache size /// (`Coordinator::session_count`) — never the shard's realm-wide total. Keying the ring by /// `(shard, gateway_key)` instead of `shard` alone means every gateway's samples survive @@ -68,7 +68,7 @@ pub struct ShardLoad { pub gateway_key: u64, } -/// The realm-wide total for one shard, MATERIALIZED at write time (issue #308's actual +/// The realm-wide total for one shard, MATERIALIZED at write time (the actual /// Done-when: `game_shard_load` alone is per-`(shard, gateway_key)` rows, and `spacetime sql` has /// no `GROUP BY`/`SUM` (`docs/danger-zones.md` §2) to fold those into a total on read — so /// [`record_shard_load`] recomputes and upserts this row on every sample instead, and an operator @@ -149,7 +149,7 @@ pub(crate) fn validate_occupancy_pct(pct: f32) -> Result<(), String> { Ok(()) } -/// Record one shard's writer-occupancy + session sample (issue #78). Operator-gated like +/// Record one shard's writer-occupancy + session sample. Operator-gated like /// [`crate::region::set_region_assignment`] — in production this is called by the gateway's own /// periodic sampling task over its privileged coordinator connection /// (that connection's identity IS the claimed operator, the same one @@ -169,7 +169,7 @@ pub fn record_shard_load( } validate_occupancy_pct(writer_occupancy_pct)?; let table = ctx.db.game_shard_load(); - // Ring-scoped per (shard, gateway_key), NOT per shard alone — issue #308: with N gateways + // Ring-scoped per (shard, gateway_key), NOT per shard alone — with N gateways // sampling the same shard, keying eviction (and therefore the ring) by shard alone let each // gateway's insert evict every OTHER gateway's rows, so only the last writer's share ever // survived. Scoping the ring per gateway too means every gateway keeps its own ~10 minutes of @@ -191,7 +191,7 @@ pub fn record_shard_load( sessions, gateway_key, }); - // Recompute and upsert the realm-wide total (issue #308's actual Done-when) rather than + // Recompute and upsert the realm-wide total (the actual Done-when) rather than // leaving that fold as a manual eyeball step — see `ShardLoadTotal`'s doc. let total_sessions = realm_wide_sessions( table @@ -213,7 +213,7 @@ pub fn record_shard_load( Ok(()) } -/// Record one region's open-world player-count sample (issue #78). Same gating and ring-buffer +/// Record one region's open-world player-count sample. Same gating and ring-buffer /// discipline as [`record_shard_load`]; see that reducer's doc. #[reducer] pub fn record_region_load( diff --git a/module/src/loot/mod.rs b/module/src/loot/mod.rs index 87875ea00..94e735b10 100644 --- a/module/src/loot/mod.rs +++ b/module/src/loot/mod.rs @@ -4,7 +4,7 @@ //! on the same killing blow; the gateway reads them to populate the loot window, and taking one //! (`CMSG_AUTOSTORE_LOOT_ITEM`) is `crate::items::take_loot` → `apply_take_loot`. [server]/[entity] //! -//! Split (issue #384) into three files that stay one Rust module (`crate::loot::*` paths are +//! Split into three files that stay one Rust module (`crate::loot::*` paths are //! unaffected — everything below re-exports through): this file (the roll core, the money split, //! and the loot-family DATA tables — creature/pickpocket/gameobject/skinning/fishing), `rolls.rs` //! (the NEED/GREED/round-robin/master group-loot machinery), and the sibling top-level @@ -49,7 +49,7 @@ use crate::character::game_character; // credit_purse's offline-recipient fallba use crate::game_group_member; // clone_quest_loot_for_group's GameObject roster read use crate::game_world_entity; use crate::quest::objective_kind; -use crate::{game_character_quest, game_quest_objective}; // killer_needs_item (fishing's zone resolve now lives in terrain::zone_id_at, #375) +use crate::{game_character_quest, game_quest_objective}; // killer_needs_item (fishing's zone resolve now lives in terrain::zone_id_at) use lyracore_shared::loot_roll::event_kind as roll_event_kind; // apply_loot_money's MONEY_SHARE push mod rolls; @@ -58,7 +58,7 @@ pub(crate) mod tag; pub use tag::*; // The profession reducers (skinning/fishing/enchanting) live in the sibling top-level -// `crate::professions` module (issue #384) — re-exported here so every existing +// `crate::professions` module — re-exported here so every existing // `crate::loot::skin_corpse`/`apply_fish`/`apply_disenchant`/`apply_enchant_item`/`entry_is_beast` // call site (debug.rs's twins) keeps compiling unchanged even though nothing in THIS file uses // them directly. @@ -202,7 +202,7 @@ pub fn scale_money_for_rank(money: u32, rank: u8) -> u32 { /// (work-item 187 slice 0): quest rows now roll into `game_corpse_loot` UNCONDITIONALLY — the /// drop-CHANCE still applies; only the old pre-roll `killer_needs_item` GATE is gone (module doc, /// decision #1's fix) — so the caller needs to know which winners were quest-only to stamp the new -/// `quest_only` column. This is the ONE roll body every family's wrapper calls (issue #384's "one +/// `quest_only` column. This is the ONE roll body every family's wrapper calls (the "one /// roll core" — `roll_loot_rows` below used to duplicate this whole algorithm minus the flag). Draws /// from the module RNG (`ctx.random`), so it has no direct unit test; its pure primitives /// (`loot_drops`/`group_pick`) are tested below. @@ -242,7 +242,7 @@ pub(crate) fn roll_loot_rows_quest_aware( /// Returns the winners as `(item_entry, count)`, in table order for independents then group order — /// callers decide what a winner MEANS (a `game_corpse_loot` row, a direct `grant_item`, …). /// -/// A thin adapter over [`roll_loot_rows_quest_aware`] (issue #384's "one roll core"): tag every row +/// A thin adapter over [`roll_loot_rows_quest_aware`] (the "one roll core"): tag every row /// `quest_only = false`, delegate, then drop the flag off each winner. The RNG draw sequence is /// IDENTICAL to the old standalone body this replaces (the same `ctx.random` calls in the same /// order), so `professions::skin_corpse`/`professions::apply_fish` — the two callers with no @@ -401,7 +401,7 @@ pub(crate) fn killer_needs_item(ctx: &ReducerContext, killer: Option, item: /// fresh roll ever writes to it. Two residue sources land here: (a) work-item 267's corpse-guid REUSE /// (a harness SQL teardown or `debug_spawn_at_feet` skipping the decay reaper leaves a departed kill's /// `game_corpse_loot_eligible` snapshot behind for the next creature minted at that guid), and (b) -/// issue #358's pickpocket/kill collision — `roll_pickpocket_loot` inserts `game_corpse_loot` rows at +/// the pickpocket/kill collision — `roll_pickpocket_loot` inserts `game_corpse_loot` rows at /// slots 0.. keyed on the LIVE creature's guid (vanilla lets a rogue crack a pocket before the kill); /// if the mob dies before every row is taken, `roll_creature_loot` re-inserts kill drops starting at /// slot 0 on the SAME guid, producing duplicate `(corpse_guid, slot)` pairs that every first-match @@ -434,7 +434,7 @@ pub(crate) fn purge_corpse_residue(ctx: &ReducerContext, guid: u64) { /// be owned by a roll that matters). This one runs when the corpse itself goes away, and must respect /// the withheld gate below. /// -/// Issue #50 fix: a `game_corpse_loot` row still `withheld` is locked by a live NEED/GREED roll — +/// Fix: a `game_corpse_loot` row still `withheld` is locked by a live NEED/GREED roll — /// which, in a sharded deployment, may be authoritative on realm-core and invisible to this /// database's own `game_loot_roll` table (promoted away within ~200ms of kill-time; see /// `creatures::tick::pass_decay`'s matching fix). Reaping it here would delete the item out from @@ -480,7 +480,7 @@ pub(crate) fn reap_corpse_loot_family(ctx: &ReducerContext, corpse_guid: u64) { /// Insert `winners` as sequential `game_corpse_loot` rows on `corpse_guid`, starting at slot 0, each /// freshly unclaimed (`reserved_for = 0`) and FFA (group-loot stamping happens AFTER this returns, in /// `apply_group_loot_rules` — never at insert time). The ONE insert loop [`roll_creature_loot`] and -/// [`roll_pickpocket_loot`] used to each carry a copy of (issue #384's dedup). +/// [`roll_pickpocket_loot`] used to each carry a copy of (the dedup). fn insert_corpse_rows(ctx: &ReducerContext, corpse_guid: u64, winners: Vec<(u32, u32, bool)>) { for (slot, (item_entry, count, quest_only)) in winners.into_iter().enumerate() { ctx.db.game_corpse_loot().insert(CorpseLoot { @@ -903,7 +903,7 @@ pub(crate) fn apply_loot_money( /// mirroring the looter's own transfer in [`apply_loot_money`]. No-op if the guid resolves to neither /// (a deleted character mid-flight) — never panics on a stale snapshot row. /// -/// DEFER verdict for the in-transit case (issue #30). This is the one audited by-guid path a REFUSAL +/// DEFER verdict for the in-transit case. This is the one audited by-guid path a REFUSAL /// gets wrong: the recipient is a PARTY MEMBER collecting their share of someone else's kill, so /// dropping the write would silently short a third party who is not transferring and cannot know /// why. `transfer::defer_money_delta` folds the copper into the escrowed export blob instead, so it @@ -1393,7 +1393,7 @@ mod tests { use crate::test_scan::code_of; - /// Issue #358: `purge_corpse_residue` is the ONE helper `kill_creature` calls to close the + /// `purge_corpse_residue` is the ONE helper `kill_creature` calls to close the /// pickpocket/kill slot collision, and the issue explicitly asks for it to cover BOTH residue /// tables (`game_corpse_loot` — the actual colliding rows — and `game_corpse_loot_eligible`, the /// pre-existing work-item-267 reused-guid residue). Losing either `by_corpse().filter(&guid)` diff --git a/module/src/loot/rolls.rs b/module/src/loot/rolls.rs index b9ecbd247..041605ebf 100644 --- a/module/src/loot/rolls.rs +++ b/module/src/loot/rolls.rs @@ -1,5 +1,5 @@ //! GROUP LOOT METHODS (work-item 187 slices 1-4) — round-robin / need-greed rolls / master looter. -//! Split out of `loot.rs` into its own submodule (issue #384): the decision enum, the rr cursor, +//! Split out of `loot.rs` into its own submodule: the decision enum, the rr cursor, //! `LootRoll`/`LootRollVote`, vote/resolve/sweep/disband, and the realm-core plane. Pure code motion //! — every gate and grant below is byte-identical to before the split. //! @@ -19,8 +19,8 @@ //! `lyracore_shared::loot_roll`'s module doc for the full rationale. The actual roll STATE //! (`LootRoll`/`LootRollVote` below) is never read by the gateway for the CLIENT's sake. //! -//! **Where the roll DECISION lives (issue #50).** `LootRoll`/`LootRollVote` are authoritative on -//! REALM-CORE, alongside `game_group`/`game_group_member` (#22) — a roll's audience is who is in the +//! **Where the roll DECISION lives.** `LootRoll`/`LootRollVote` are authoritative on +//! REALM-CORE, alongside `game_group`/`game_group_member` — a roll's audience is who is in the //! group, not where anyone stands, exactly like the membership it is snapshotted from. Kill-time //! creation (`start_roll`, below) still runs on the corpse's own WORLD SHARD unconditionally — combat //! resolution has no path to another database — so in a sharded deployment this write is a TRANSIENT @@ -43,12 +43,12 @@ //! computed spatially at kill time and stays world-shard-local too — it is `apply_group_loot_rules`'s //! own `recipients` snapshot, not part of the roll's realm-core state. //! -//! **The new case (issue #50).** A roll's PARTICIPANTS could not previously be on different shards — +//! **The new case.** A roll's PARTICIPANTS could not previously be on different shards — //! `kill_reward_recipients` only ever names members who are physically near the corpse, i.e. on its -//! own shard, at the moment the roll starts. What #50 makes possible is a participant LEAVING that -//! shard mid-roll (a portal, a dungeon entry) during the 60s window. Pre-#50 that voter's row was +//! own shard, at the moment the roll starts. The new case is a participant LEAVING that +//! shard mid-roll (a portal, a dungeon entry) during the 60s window. Before, that voter's row was //! simply unreachable from wherever they went — their vote auto-passed at the deadline, same as being -//! offline. Post-#50 it is reachable: voting is realm-core state, and the gateway routes a vote +//! offline. Now it is reachable: voting is realm-core state, and the gateway routes a vote //! through whichever shard the player is CURRENTLY on, so a mid-roll shard-hopper can still vote from //! their new location. No special-casing was needed for this — it falls out of routing votes to //! realm-core rather than to "the shard that created the roll". @@ -329,7 +329,7 @@ pub(crate) fn apply_group_loot_rules(ctx: &ReducerContext, corpse_guid: u64, gro /// Insert one [`LootRoll`] row + one [`LootRollVote`] per recipient (`voted=false`), with NO event /// push. The shared core of [`start_roll`] (kill time — this database keeps the roll unsharded, or -/// stages it for promotion sharded) and `realm_loot_op`'s `loot_op::START` arm (issue #50 — this +/// stages it for promotion sharded) and `realm_loot_op`'s `loot_op::START` arm (this /// database IS realm-core, RE-CREATING a roll a world shard already announced locally; pushing /// `ROLL_START` again here would double the popup). Returns the new roll's id. fn insert_roll_rows( @@ -364,7 +364,7 @@ fn insert_roll_rows( /// Spawn a NEED/GREED/NBG roll at KILL TIME: [`insert_roll_rows`] + `ROLL_START` to every recipient. /// -/// Issue #50: this ALWAYS runs on the corpse's own WORLD SHARD — kill-time combat resolution has no +/// This ALWAYS runs on the corpse's own WORLD SHARD — kill-time combat resolution has no /// path to another database — so in a SHARDED deployment the row it creates is a TRANSIENT staging /// copy: the gateway's loot-roll relay (`gateway/src/world/loot.rs::relay_tick`) promotes it onto /// realm-core and clears this copy (`clear_promoted_loot_roll`) so voting/resolution never runs @@ -397,12 +397,12 @@ fn start_roll( } } -/// The identity-free vote core (issue #50, mirrors `group.rs`'s `*_on` cores): shared by the player +/// The identity-free vote core (mirrors `group.rs`'s `*_on` cores): shared by the player /// reducer above (the world shard the voter is physically standing on) and `realm_loot_op`'s VOTE arm /// (realm-core, once the roll has been promoted there). Same gates, same resolution, in the same /// order — `loot_roll` is byte-identical for clients. Unlike a group op, nothing about a vote's RULES /// differs by which database it runs on, only which one `game_loot_roll`'s `ctx.db` resolves to — the -/// caller already picked that by calling this on the right connection, so (issue #384 smalls) there is +/// caller already picked that by calling this on the right connection, so (smalls) there is /// no `plane` parameter to thread through. pub(crate) fn cast_vote_on( ctx: &ReducerContext, @@ -465,7 +465,7 @@ pub(crate) fn cast_vote_on( Ok(()) } -/// Grant a resolved roll's item to its winner, on WHICHEVER database holds the corpse (issue #50). +/// Grant a resolved roll's item to its winner, on WHICHEVER database holds the corpse. /// /// Called inline by [`resolve_roll`] / [`force_resolve_rolls_for_disband`], wherever they execute — /// a single database, or REALM-CORE in a sharded one — and by the [`settle_loot_roll`] operator @@ -493,7 +493,7 @@ pub(crate) fn settle_roll_grant( .filter(&corpse_guid) .find(|l| l.slot == slot) else { - // #99 AC#4 sweep. This return is the one that ATE A WINNER'S ITEM (#50's review): a slow + // AC#4 sweep. This return is the one that ATE A WINNER'S ITEM (the review): a slow // voter had `CORPSE_DECAY_MICROS == ROLL_WINDOW_MICROS` reap the corpse out from under the // roll, settlement found no row, and it returned with no grant, no error and no log. // @@ -579,7 +579,7 @@ fn resolve_roll(ctx: &ReducerContext, roll: &LootRoll, votes: &[LootRollVote], r let winning_vote = votes[tier[w]].vote; // NEED or GREED — the tier that actually won (finding #3) // Grant, then finalize the CorpseLoot row per the outcome (module doc: 068 mail is deferred). - // Issue #50: on a single database this runs inline, right here, exactly as before. On REALM-CORE + // On a single database this runs inline, right here, exactly as before. On REALM-CORE // (sharded) `game_corpse_loot` is always empty — no kills happen there — so this no-ops for free, // and the ACTUAL grant happens on the corpse's own world shard via `settle_loot_roll`, which the // gateway's loot-roll relay calls after observing the `ROLL_WON` event pushed below. @@ -688,7 +688,7 @@ pub(crate) fn force_resolve_rolls_for_disband( }); match survivor.filter(|s| all.iter().any(|v| v.voter_guid == *s)) { Some(winner_guid) => { - // Issue #50: inline on a single database (unchanged), a free no-op on REALM-CORE + // Inline on a single database (unchanged), a free no-op on REALM-CORE // (no local `game_corpse_loot` row there) — the real grant then settles onto the // corpse's own world shard via `settle_loot_roll`, driven by the gateway's loot-roll // relay off the `ROLL_WON` event pushed below. See `settle_roll_grant`'s own doc. @@ -717,12 +717,12 @@ pub(crate) fn force_resolve_rolls_for_disband( } // =========================================================================================== -// REALM-CORE plane (issue #50) — mirrors `group.rs`'s realm-core section for the roll itself. +// REALM-CORE plane — mirrors `group.rs`'s realm-core section for the roll itself. // =========================================================================================== // `#[reducer]`: SpacetimeDB reducers take their arguments FLAT off the wire, so a parameter struct is not available. #[allow(clippy::too_many_arguments)] -/// The realm-wide loot-roll ops (issue #50), as ONE operator-gated reducer keyed by +/// The realm-wide loot-roll ops, as ONE operator-gated reducer keyed by /// [`lyracore_shared::loot_roll::loot_op`] — the same one-reducer-not-several trade `realm_group_op` /// made (`lyracore_shared::group::realm_op`'s doc), for the same reason: every gateway-callable reducer /// needs a hand-maintained SDK binding (`docs/danger-zones.md` §1.2). @@ -765,7 +765,7 @@ pub fn realm_loot_op( } } -/// Grant a resolved roll's item on the WORLD SHARD that actually holds the corpse (issue #50). The +/// Grant a resolved roll's item on the WORLD SHARD that actually holds the corpse. The /// gateway's loot-roll relay calls this on every connected world shard after observing realm-core's /// `ROLL_WON` event — [`settle_roll_grant`]'s own guards make every wrong-shard call a harmless /// no-op, so the relay never needs to know in advance WHICH shard holds the corpse. @@ -782,7 +782,7 @@ pub fn settle_loot_roll( } /// Delete a STAGING roll's rows on the WORLD SHARD that created them, once the gateway's loot-roll -/// relay has promoted the roll onto realm-core (issue #50) — [`cleanup_roll`], the same delete a +/// relay has promoted the roll onto realm-core — [`cleanup_roll`], the same delete a /// resolved roll already used, called here for an UNRESOLVED one that just changed which database is /// authoritative for it. #[reducer] @@ -792,7 +792,7 @@ pub fn clear_promoted_loot_roll(ctx: &ReducerContext, roll_id: u64) -> Result<() Ok(()) } -/// The identity-free master-give core (#479, the `cast_vote_on` shape): everything +/// The identity-free master-give core (the `cast_vote_on` shape): everything /// [`loot_master_give`] does after resolving WHO the master looter is. `gw::gw_loot_master_give` is /// the other entry. pub(crate) fn apply_master_give( @@ -944,11 +944,11 @@ mod tests { assert!(pick_roll_winner(&[]).is_none()); } - // ---- The realm-core loot-roll plane (issue #50) ---- + // ---- The realm-core loot-roll plane ---- // // A reducer body needs a live `ReducerContext`, so none of these can be EXECUTED by a test in // this crate — exactly why they are scanned, the same technique `group.rs` uses for its own - // realm-plane reducers (issue #22's review of PR #49). + // realm-plane reducers (the review). use crate::test_scan::code_of; @@ -1023,7 +1023,7 @@ mod tests { ); } - /// Issue #50's own acceptance test: `group::remove_member`'s disband branch must call + /// The acceptance test for this: `group::remove_member`'s disband branch must call /// `force_resolve_rolls_for_disband` UNCONDITIONALLY and BEFORE it tears the group row down. /// That ordering — not a `Plane` flag, which does not exist on `remove_member` — is what makes a /// disbanding party's live rolls resolve on the SAME database, in the SAME transaction, as the diff --git a/module/src/motion.rs b/module/src/motion.rs index d3f270045..cdb74a78c 100644 --- a/module/src/motion.rs +++ b/module/src/motion.rs @@ -1,4 +1,4 @@ -//! Batched movement republish (#461) — the private staging table + the scheduled reducer that +//! Batched movement republish — the private staging table + the scheduled reducer that //! drains it into the public `game_entity_motion` relay. //! //! # Why @@ -50,7 +50,7 @@ use crate::{game_entity_motion, game_world_entity, EntityMotion}; // Cadence // =========================================================================================== -/// Default republish cadence — 50 000 µs = **20 Hz** (#461's default). Peer movement gains at most +/// Default republish cadence — 50 000 µs = **20 Hz** (the default). Peer movement gains at most /// one tick (50 ms) of latency; vanilla clients dead-reckon between updates, and BitCraft ships far /// coarser (one row per path segment, lerped on read). pub const MOTION_TICK_MICROS: i64 = 50_000; @@ -84,7 +84,7 @@ pub struct PendingMotion { pub grid_y: i32, /// `(grid_x, grid_y)` packed — the AOI cell key, carried through to the public row verbatim. /// Written in the SAME statement as `grid_x`/`grid_y` from - /// `lyracore_shared::spatial::grid_cell_id`, per #456 (see `tripwires::grid_cell_tripwire`). + /// `lyracore_shared::spatial::grid_cell_id` (see `tripwires::grid_cell_tripwire`). pub cell: i64, pub opcode: u16, pub movement_info: Vec, @@ -195,7 +195,7 @@ pub(crate) fn tick_action(entity_alive: bool) -> TickAction { // =========================================================================================== /// Publish ONE staged payload into the public `game_entity_motion` relay, minting `seq` off the -/// public row (`+1`, or 0 for a mover's first ever motion) exactly as the pre-#461 inline relay did. +/// public row (`+1`, or 0 for a mover's first ever motion) exactly as the earlier inline relay did. fn publish(ctx: &ReducerContext, p: &PendingMotion) { let motions = ctx.db.game_entity_motion(); match motions.guid().find(p.guid) { @@ -313,7 +313,7 @@ pub(crate) fn drop_pending(ctx: &ReducerContext, guid: u64) { // The tick // =========================================================================================== -/// **The 20 Hz republish (#461).** Drains `game_entity_motion_pending` into the public +/// **The 20 Hz republish.** Drains `game_entity_motion_pending` into the public /// `game_entity_motion` in ONE transaction, so SpacetimeDB's subscription sweep runs once per /// firing instead of once per movement packet. /// @@ -334,8 +334,8 @@ pub fn publish_motion(ctx: &ReducerContext, _schedule: MotionPublishSchedule) { } /// The drain behind [`publish_motion`] — republish every staged mover into the public relay. -/// #482 also runs this at the END of `gw_movement_batch`, INSIDE the batch transaction: the -/// subscription sweep (the whole reason the pending stage exists, #461) is paid once per batch +/// Also runs this at the END of `gw_movement_batch`, INSIDE the batch transaction: the +/// subscription sweep (the whole reason the pending stage exists) is paid once per batch /// either way, and draining inline removes the schedule's 50ms latency quantum plus the /// pending-row churn for the gateway path. The scheduled tick keeps running for the legacy /// per-player path and is a cheap no-op when the batch already drained. @@ -646,7 +646,7 @@ mod tests { for guid in 0..100u64 { w.alive.insert(guid); } - // Three heartbeats per player inside one window (the pre-#461 cost: 300 transactions). + // Three heartbeats per player inside one window (the earlier cost: 300 transactions). for _ in 0..3 { for guid in 0..100u64 { w.movement(guid, HEARTBEAT, &[0x01]); diff --git a/module/src/nav.rs b/module/src/nav.rs index 2841e977b..c07d0bfa4 100644 --- a/module/src/nav.rs +++ b/module/src/nav.rs @@ -234,7 +234,7 @@ fn fetcher(ctx: &ReducerContext, map_id: u32) -> impl FnMut(u16, u16) -> Option< } /// Line of sight for gameplay checks (aggro sensing, hostile casts, engage, swing gate, caster -/// hold-range). Issue #523 (decision #10 §10, `#522`'s benchmark PASS): when exact vmap data is +/// hold-range) (decision §10, benchmark PASS): when exact vmap data is /// consuming (`vmap::vmap_enabled`), this defers to the exact WMO-only ray /// (`vmap::los_ray`) instead of the coarse grid raymarch — a hostile behind a real wall is /// "unseen" at the true wall plane rather than the nearest whole nav cell. Falls back to the grid @@ -264,7 +264,7 @@ const LEG_MAX_EXPANSIONS: u32 = 4096; /// A missing path may aim straight at the goal, but the commit gate stops it at obstructions. /// /// PERF NOTE: the gate fires on every committed movement step for chase/return/wander/flee/ -/// pet-follow — the ~500ms movement tick, 8x more often than the ~4s sense tick `#522`'s benchmark +/// pet-follow — the ~500ms movement tick, 8x more often than the ~4s sense tick the benchmark /// (`debug_bench_los`) measured `vmap::los_ray` against. `debug_bench_collision_gate` /// (`debug/instance.rs`) is the companion harness for THIS path — run it against the same box /// before flipping `vmap_enabled` on a populated map, and compare its wall time to diff --git a/module/src/professions.rs b/module/src/professions.rs index f1707fa07..d3ca1a9ea 100644 --- a/module/src/professions.rs +++ b/module/src/professions.rs @@ -1,5 +1,5 @@ //! Profession reducers that are feature logic layered OVER a corpse or gear slot, not loot-table -//! machinery themselves (issue #384): SKINNING (a skill-gated corpse interaction modeled on +//! machinery themselves: SKINNING (a skill-gated corpse interaction modeled on //! `loot::loot_money`), FISHING (an immediate-catch cast), and ENCHANTING (the per-instance //! `enchant_id` overlay — which never touches a corpse at all, the reason this split exists). //! @@ -260,7 +260,7 @@ pub(crate) fn apply_fish(ctx: &ReducerContext, guid: u64) -> Result<(), String> crate::skill::skill_line::FISHING, crate::skill::APPRENTICE_CAP, ); - // Zone-keyed catch (work-item 210, resolver hoisted to `terrain::zone_id_at` by #375): resolve the + // Zone-keyed catch (work-item 210, resolver hoisted to `terrain::zone_id_at`): resolve the // caster's zone and roll its real table; an unresolved zone (unimported terrain/AreaTable, or // off-slice) rolls an empty Vec, which `roll_loot_rows` correctly turns into zero winners — // `pick_caught_fish` then floors to the flat pool either way. `?`-rollback: a full bag fails here diff --git a/module/src/publish_safety.rs b/module/src/publish_safety.rs index 74220ed81..601c2f04b 100644 --- a/module/src/publish_safety.rs +++ b/module/src/publish_safety.rs @@ -138,10 +138,10 @@ fn shell_shape(src: &str) -> String { /// The scan roots, each with its own file FLOOR: `(dir, minimum *.sh/*.py files)`. /// -/// `adapters/` joined the list with #246, when the wire harness moved to its own repository: the +/// `adapters/` joined the list, when the wire harness moved to its own repository: the /// live-test entrypoint that survived here (`adapters/lyracore/run-suite.sh`) shells out against a /// running stack, which is exactly the shape of script this scan exists to police. `importer/` -/// joined it with #323, when the world-import ETL moved out of `scripts/` and into +/// joined it, when the world-import ETL moved out of `scripts/` and into /// `importer/scripts/` — it is the same seven scripts, and one of them DOCUMENTS a publish command /// in its abort message. A root that is not scanned is a root where a `spacetime publish -c` can /// sit unnoticed. @@ -545,11 +545,11 @@ spacetime publish -cy lyracore /// Sanity FLOOR. A scan rooted at the wrong directory finds zero files, flags zero violations /// and reports success — so the floor is what makes a green result mean something. /// - /// The floor was 40 until #246. It is 25 now, and the drop is a REAL loss of surface, not a + /// The floor was 40 once. It is 25 now, and the drop is a REAL loss of surface, not a /// relaxed standard: 47 of the files this used to scan were the wire harness's live-stack /// orchestrators under `tools/wire-client/`, and they are in another repository now. What is /// left (26 in `scripts/` + `tools/` + `importer/scripts/`, plus `adapters/lyracore/run-suite.sh`) - /// is every script this repo still ships. #323 moved seven of those files from `scripts/` to + /// is every script this repo still ships. A later change moved seven of those files to /// `importer/scripts/` and added that root to the scan in the same change, so the count did not /// move — a relocation must never be allowed to shrink this number by leaving a root behind. /// Lower it again only alongside a comparable structural move, and never to make a red run diff --git a/module/src/quest.rs b/module/src/quest.rs index 8fef0bd15..cb9342403 100644 --- a/module/src/quest.rs +++ b/module/src/quest.rs @@ -411,7 +411,7 @@ crate::character_owned!(delete, fn sweep_delete_game_character_quest(ctx, charac quests.id().delete(r.id); } }); -// CROSS-DATABASE transport (issue #19): the quest log IS the run — a Deadmines party arriving with +// CROSS-DATABASE transport: the quest log IS the run — a Deadmines party arriving with // an empty log could not turn anything in. `id` is a surrogate PK, re-minted. crate::character_owned!(transfer, fn sweep_transfer_game_character_quest(ctx, character_guid, io) { table = game_character_quest, @@ -2260,7 +2260,7 @@ crate::game_tick_pass!(fn quest_timer_pass(ctx) { let log = ctx.db.game_character_quest(); // `is_expired`'s own `deadline_micros != 0` check makes this a cheap no-op scan for the (typical) // all-untimed quest log — no separate index needed for this table's expected small size. - // REFUSE verdict (issue #30) — the production twin of `debug::debug_expire_quest`, fenced there + // REFUSE verdict — the production twin of `debug::debug_expire_quest`, fenced there // for exactly this reason: the write lands in `game_character_quest`, a MANIFEST table, so // post-`begin_transfer` it is a lost write cross-database. This pass is BACKGROUND, so the // "the guid came from a live entity" argument that fences the rest of `quest.rs` does not reach @@ -2408,7 +2408,7 @@ pub(crate) fn debug_force_expire( // Player reducers — authorized via ctx.sender, delegate to the cores // =========================================================================================== -/// The abandon core, actor-explicit (#479) — the body [`abandon_quest`] used to inline, shared with +/// The abandon core, actor-explicit — the body [`abandon_quest`] used to inline, shared with /// `gw::gw_abandon_quest`. pub(crate) fn apply_abandon_quest( ctx: &ReducerContext, diff --git a/module/src/realm_core.rs b/module/src/realm_core.rs index 86cb624b6..b3ca1875b 100644 --- a/module/src/realm_core.rs +++ b/module/src/realm_core.rs @@ -1,4 +1,4 @@ -//! Realm-core: the realm-wide **character→shard index** (issue #20, spec issue #12). +//! Realm-core: the realm-wide **character→shard index** (spec). //! //! # What "realm-core" is //! @@ -56,7 +56,7 @@ crate::character_owned!(delete, fn sweep_delete_game_character_shard(ctx, charac ctx.db.game_character_shard().character_guid().delete(character_guid); }); -// CROSS-DATABASE transport (issue #19 × #20): the directory entry deliberately does NOT ride the +// CROSS-DATABASE transport: the directory entry deliberately does NOT ride the // export blob. It is not character data — it is a routing HINT about WHERE the character is, and the // blob's whole purpose is to change that. The source's row is snapshotted by `begin_transfer` while // it still names the SOURCE, so importing it would give the destination a forwarding receipt @@ -65,13 +65,13 @@ crate::character_owned!(delete, fn sweep_delete_game_character_shard(ctx, charac // directory honest are `transfer::do_finish` (the source's own row, rewritten to name the // destination, in the escrow's transaction) and the gateway's `set_character_shard` against // realm-core, which is the authoritative copy — driven as a required step of -// `world::transfer::run_transfer` immediately after `finish_transfer` commits (issue #34), and +// `world::transfer::run_transfer` immediately after `finish_transfer` commits, and // re-driven by the login self-heal probe whenever the two disagree. crate::character_owned!(not_transported, fn sweep_transfer_game_character_shard()); /// Upsert the index entry for `character_guid`. Shared by [`set_character_shard`] (the gateway's /// write, on realm-core) and by `transfer::do_finish` (the module's write, in the SAME transaction -/// that releases the escrow — issue #20 AC#3). +/// that releases the escrow — AC#3). pub(crate) fn record_shard( ctx: &ReducerContext, character_guid: u64, @@ -97,13 +97,13 @@ pub(crate) fn record_shard( /// input, so a client that could write it could redirect another player's login. /// /// Two callers, both on the gateway's realm-core handle: `world::transfer::run_transfer` publishes -/// the settled destination here as a required step of every escrowed transfer (issue #34 — strictly +/// the settled destination here as a required step of every escrowed transfer (strictly /// after `finish_transfer` committed, so it can only ever name a destination the escrow reached), /// and `home_shard`'s login self-heal rewrites it whenever the fallback probe disagrees with what /// the index says. Only the FIRST of those runs in production today: the gateway's world-entry /// resolver is `settle_home_shard`, which overrides `home_shard` and scans the shards instead of /// consulting this table, so the index is currently written-but-never-read and its self-heal never -/// fires. #39/#21 is what changes that. +/// fires. The instance-placement work is what changes that. #[reducer] pub fn set_character_shard( ctx: &ReducerContext, @@ -117,11 +117,11 @@ pub fn set_character_shard( } // --------------------------------------------------------------------------------------------- -// Guid-range registry (#108) — realm-core answers "which shard mints from where" +// Guid-range registry — realm-core answers "which shard mints from where" // --------------------------------------------------------------------------------------------- /// How many guids a slot owns. Slot *n* is `[n · GUID_RANGE_SIZE, (n+1) · GUID_RANGE_SIZE)`, which -/// makes #105's hand-applied floors (core 0, world-1 1e9, instances 2e9, realm-core 3e9) slots +/// makes the hand-applied floors (core 0, world-1 1e9, instances 2e9, realm-core 3e9) slots /// 0/1/2/3 exactly — so the live databases migrate into this registry by being *recorded*, with no /// guid changing. Decimal and small on purpose (`GuidRange`'s doc, `auth.rs`): readable in logs and SQL, /// a billion characters per shard, and far below `2^53`, above which `spacetime call` mangles a u64 @@ -218,7 +218,7 @@ mod guid_range_registry_tests { #[test] fn a_shard_keeps_the_slot_it_is_already_minting_from_whatever_the_claim_order() { - // The four live databases, by their #105 floors — claimed in the WORST order (highest + // The four live databases, by their floors — claimed in the WORST order (highest // first), which is exactly what a `max + 1` allocator would mangle. let live = [ ("world-1", 1_000_000_101u64), diff --git a/module/src/region.rs b/module/src/region.rs index 66f45ca1c..a07eda10c 100644 --- a/module/src/region.rs +++ b/module/src/region.rs @@ -1,4 +1,4 @@ -//! Regions: the cell→region→shard hierarchy as DATA (issue #23, spec #12). +//! Regions: the cell→region→shard hierarchy as DATA (spec). //! //! Two tables, one job each, and **no game logic in this file reads either of them**: //! @@ -23,7 +23,7 @@ //! Nothing here migrates anything. With every region of a map assigned to one shard — or with no //! assignment rows at all, the default — routing is a strict no-op and the seams are dormant. //! View-merge, warm handoff, relayed intents and live migration are later tickets, gated on the -//! #18 benchmark showing a tuned writer actually saturating. +//! Benchmark showing a tuned writer actually saturating. use lyracore_shared::region::RegionMap; use spacetimedb::{reducer, table, ReducerContext, Table}; diff --git a/module/src/reputation.rs b/module/src/reputation.rs index ad54942b2..17646edfb 100644 --- a/module/src/reputation.rs +++ b/module/src/reputation.rs @@ -26,7 +26,7 @@ pub struct PlayerReputation { // END-APPENDED (#[default(0)] → additive auto-migrate; negative defaults aren't accepted by the macro). // The Faction.dbc ReputationListID — the SMALL 0..63 index the 5875 client's SMSG_SET_FACTION_STANDING // addresses in its rep array. The client does NOT key on faction_id: sending faction_id 72 (Stormwind) - // instead of its rep-index 19 indexes PAST the 64-slot array → null deref → ERROR #132 crash on the first + // instead of its rep-index 19 indexes PAST the 64-slot array → null deref → ERROR crash on the first // rep-granting turn-in (Marshal McBride). We STORE it here (grant_reputation has the faction lookup) so the // gateway relay sends the right index without joining game_faction (which it doesn't bind). grant_reputation // only ever writes rows for rep-bar factions and ALWAYS stamps the real index, so a row never carries the @@ -49,7 +49,7 @@ crate::character_owned!(delete, fn sweep_delete_game_player_reputation(ctx, char reps.id().delete(r.id); } }); -// CROSS-DATABASE transport (issue #19): standings are durable progression, and the login +// CROSS-DATABASE transport: standings are durable progression, and the login // SMSG_INITIALIZE_FACTIONS is built from them — a character arriving without them shows every // faction at neutral. `id` is a surrogate PK, re-minted. crate::character_owned!(transfer, fn sweep_transfer_game_player_reputation(ctx, character_guid, io) { @@ -228,7 +228,7 @@ pub(crate) fn grant_reputation( }); } -/// The At-War core, actor-explicit (#479): everything [`set_faction_at_war`] does after resolving +/// The At-War core, actor-explicit: everything [`set_faction_at_war`] does after resolving /// WHOSE rep pane this is. Takes the row — the seed arm stamps `owner_identity` off it. pub(crate) fn apply_set_faction_at_war( ctx: &ReducerContext, diff --git a/module/src/rest.rs b/module/src/rest.rs index 81eb21014..c8ba881ca 100644 --- a/module/src/rest.rs +++ b/module/src/rest.rs @@ -70,7 +70,7 @@ crate::character_owned!(delete, fn sweep_delete_game_rest_state_event(ctx, chara ev.id().delete(r.id); } }); -// CROSS-DATABASE transport (issue #19): a rest-state event is a one-shot RELAY row with a GC TTL — +// CROSS-DATABASE transport: a rest-state event is a one-shot RELAY row with a GC TTL — // the gateway turns it into one PLAYER_BYTES_2 packet and it is done. It carries no durable state // (the durable half is `Character.resting`/`rested_xp`, which rides the character row), so carrying // it would replay a stale zzz-icon flip at the destination. Exports NOTHING, deliberately: the arm @@ -174,7 +174,7 @@ crate::game_tick_pass!(fn rested_accrue_pass(ctx) { // Deliberate simplification: full game_character scan every 30s; add a `by_resting` index if // the table grows large. // - // REFUSE verdict (issue #30). This pass is BACKGROUND, not caller-driven, so "the guid came from + // REFUSE verdict. This pass is BACKGROUND, not caller-driven, so "the guid came from // a live entity" — the argument that fences every other rest write — does not apply: the filter // reads the DURABLE row's flags and nothing here ever looks at `game_world_entity`. // `begin_transfer` persists with `set_offline: false`, and it is the `set_offline` branch of diff --git a/module/src/seed.rs b/module/src/seed.rs index 7fed76651..da25244df 100644 --- a/module/src/seed.rs +++ b/module/src/seed.rs @@ -1,5 +1,5 @@ //! The `init` lifecycle reducer — the single entrypoint that populates a fresh database. `init` -//! itself is a four-line dispatcher (#377) over four banner-stratum fns, each a straight +//! itself is a four-line dispatcher over four banner-stratum fns, each a straight //! extraction of what used to be one ~1,600-line function (a reader still sees the whole seed by //! reading top to bottom — the split is fn boundaries, not a reorder): //! @@ -7,7 +7,7 @@ //! fallback graveyard/graveyard-zone rows (work-item 209), the TEST account + pre-seeded //! character (with its starter spellbook/action-bar kit), and the EventAI on-aggro barks. Every //! fresh database needs this regardless of whether it will ever host a real import. -//! 2. **`seed_map0_demo_content`** (the in-body `DECISION (issue #79)` comment has the full +//! 2. **`seed_map0_demo_content`** (the in-body `DECISION` comment has the full //! reasoning): NPCs, a starter weapon, profession items, a skinning beast, a profession trainer, //! gameobjects, gather nodes, a tier-variety demonstrator, and the temporary Elwynn Forest / //! Westfall weather climate, each under its own `// ---` banner. @@ -50,7 +50,7 @@ use crate::{game_alpha_test_tools_enrollment, AlphaTestToolsEnrollment}; #[reducer(init)] pub fn init(ctx: &ReducerContext) { - // Four banner strata (#377 split these out of what used to be one ~1,600-line fn — see this + // Four banner strata (split these out of what used to be one ~1,600-line fn — see this // file's header for what each one seeds and why the split points fall where they do). Order // matters: later strata reference nothing from earlier ones (each re-derives its own `hw` // alias), but the production core must exist before anything reads `game_config`/`game_realm`, @@ -82,14 +82,14 @@ fn seed_production_core(ctx: &ReducerContext) { // Server tunables: the xp_rate singleton starts Blizzlike (1.0×). Admins tune it via SQL // (`UPDATE game_config SET xp_rate = N WHERE id = 0`) or the `debug_set_xp_rate` reducer. // `hosts_instances` starts TRUE — a fresh single-database realm spawns dungeon populations - // itself, exactly as before #39. A multi-database deployment turns it off on the world shard. + // itself, exactly as before. A multi-database deployment turns it off on the world shard. ctx.db.game_config().insert(ServerConfig { id: 0, xp_rate: 1.0, nav_enabled: true, hosts_instances: true, bots_idle: false, // bots think by default; the load-test lever freezes them - vmap_enabled: false, // #521/#523: off until an operator imports vmap data + flips it + vmap_enabled: false, // Off until an operator imports vmap data + flips it nav_coverage_enabled: false, // off until an operator prepares coverage + flips it }); @@ -236,7 +236,7 @@ fn seed_production_core(ctx: &ReducerContext) { crate::creatures::seed_on_aggro_fixtures(ctx); } -/// Stratum 2 — Map-0 (Northshire) demo/fixture content (the `DECISION (issue #79)` comment below +/// Stratum 2 — Map-0 (Northshire) demo/fixture content (the `DECISION` comment below /// has the full reasoning): NPCs, a starter weapon, profession items, a skinning beast, a /// profession trainer, gameobjects, gather nodes, a tier-variety demonstrator, and the temporary /// Elwynn Forest / Westfall weather climate. Every row here is wholesale-replaced the moment a real @@ -248,7 +248,7 @@ fn seed_map0_demo_content(ctx: &ReducerContext) { seed_taxi_fixture(ctx); - // DECISION (issue #79): everything from here down through the gather-pool block seeds MAP-0 + // DECISION: everything from here down through the gather-pool block seeds MAP-0 // (Northshire) spatial content — 4 creature spawns (Chicken 620, Test Wolf 51000, Profession // Trainer 51001, Test Flight Master 51006) and up to 5 live `game_gameobject` rows (the // chest/goober/2 standalone gather nodes + the tier-pool's one armed point) — into EVERY freshly @@ -381,7 +381,7 @@ fn seed_map0_demo_content(ctx: &ReducerContext) { // is demonstrable: its 8–12 / 2.6s profile is clearly above the Worn Shortsword's 1–3, so equipping // it visibly raises the swing readout. Hand-authored reference data (licensing firewall: never // bulk-imported), display 1542 ships in 5875. inventory_type 21 = main-hand, quality 2 = Uncommon. - // Canonical constructor shared with the fixture-restore path (`tempered_blade_template`, #363) — + // Canonical constructor shared with the fixture-restore path (`tempered_blade_template`) — // the reserved-id copy under FIXTURE_BLADE stays in sync with this one by construction now. ctx.db .game_item_template() @@ -405,7 +405,7 @@ fn seed_map0_demo_content(ctx: &ReducerContext) { bonding: crate::items::bonding::NONE, // plain common gear — unbound/tradeable ..base_item(51, "Recruit's Tunic") }); - // Canonical constructor shared with the fixture-restore path (`tough_jerky_template`, #363). + // Canonical constructor shared with the fixture-restore path (`tough_jerky_template`). ctx.db.game_item_template().insert(tough_jerky_template(52)); // --- PROFESSION ITEMS: every profession reagent/product/yield points at a REAL vanilla @@ -437,7 +437,7 @@ fn seed_map0_demo_content(ctx: &ReducerContext) { // are Timestamps) — the parent re-imports OR `debug_spawn_at_feet(guid, 51000)` materializes a live // wolf. type_flags 0x100 = SKINNABLE (the skin gate keys on creature_type==1 alone; the flag is // carried for data parity). - // Canonical constructor shared with the fixture-restore path (`test_wolf_template`, #363) — + // Canonical constructor shared with the fixture-restore path (`test_wolf_template`) — // the post-import restore reducer builds the same row from the same fn, so a shard restored // after an ETL wipe can never drift from a freshly-published one again. let wolf_tmpl = ctx.db.game_creature_template().insert(test_wolf_template()); @@ -472,8 +472,8 @@ fn seed_map0_demo_content(ctx: &ReducerContext) { // 50085→171 Alchemy, 50086→129 First Aid, 50087→197 Tailoring, 50088→164 Blacksmithing — // Smelting rides Mining, NO offering) are NOT seeded here — `game_trainer_spell` is populated for // this entry by the world-import ETL instead. - // Canonical constructor shared with the fixture-restore path (`profession_trainer_template`, - // #363) — same drift-proofing as the Test Wolf above. + // Canonical constructor shared with the fixture-restore path (`profession_trainer_template`) + // — same drift-proofing as the Test Wolf above. let trainer_tmpl = ctx .db .game_creature_template() @@ -501,7 +501,7 @@ fn seed_map0_demo_content(ctx: &ReducerContext) { // The Hearthstone (entry 6948) — every character starts with one (granted in `grant_starter_item`); // using it recalls to the bound home (`Character::home_*`). Real vanilla values: class 15 (Misc), // display 6418 (the hearthstone icon, ships in 5875), unsellable, non-stacking, no stats. - // spellid_1/spelltrigger_1 (#387) name its on-use spell — "Call Stone" (50119, seeded in + // spellid_1/spelltrigger_1 name its on-use spell — "Call Stone" (50119, seeded in // `seed_spell_registry` below), trigger 0 (on-use). `apply_item_use` now reads spellid_1 as the // single on-use authority for EVERY item, so this is what makes the Hearthstone usable at all; // the old hardcoded entry-id special case in items::ops is retired. @@ -616,7 +616,7 @@ fn seed_map0_demo_content(ctx: &ReducerContext) { rotation_0: 0.0, rotation_1: 0.0, rotation_2: 0.0, - rotation_3: 0.0, // seed fixtures orient via `orientation` only; codec derives yaw (#515) + rotation_3: 0.0, // seed fixtures orient via `orientation` only; codec derives yaw }); ctx.db .game_gameobject_template() @@ -651,7 +651,7 @@ fn seed_map0_demo_content(ctx: &ReducerContext) { rotation_0: 0.0, rotation_1: 0.0, rotation_2: 0.0, - rotation_3: 0.0, // seed fixtures orient via `orientation` only; codec derives yaw (#515) + rotation_3: 0.0, // seed fixtures orient via `orientation` only; codec derives yaw }); // --- GATHER nodes: a Copper Vein (MINING) + a Peacebloom (HERBALISM) by the player spawn so the @@ -692,7 +692,7 @@ fn seed_map0_demo_content(ctx: &ReducerContext) { rotation_0: 0.0, rotation_1: 0.0, rotation_2: 0.0, - rotation_3: 0.0, // seed fixtures orient via `orientation` only; codec derives yaw (#515) + rotation_3: 0.0, // seed fixtures orient via `orientation` only; codec derives yaw }); ctx.db .game_gameobject_template() @@ -727,7 +727,7 @@ fn seed_map0_demo_content(ctx: &ReducerContext) { rotation_0: 0.0, rotation_1: 0.0, rotation_2: 0.0, - rotation_3: 0.0, // seed fixtures orient via `orientation` only; codec derives yaw (#515) + rotation_3: 0.0, // seed fixtures orient via `orientation` only; codec derives yaw }); // --- TIER-VARIETY DEMONSTRATOR (gather multinodes): an IN-PLACE Copper point that ~15% of the time @@ -1011,7 +1011,7 @@ fn seed_spell_registry(ctx: &ReducerContext) { // auto-migrate publish, so an already-migrated dev DB re-seeds via the debug reducer (same // precedent as `talent::seed_talents`/`debug_seed_talents`). seed_pw_shield_fixture(ctx); // Weakened Soul (6788) + Test PW:Shield (50072) — linked-debuff mechanic - // issue #85: the scenario-fixture items (Tempered Blade/Tough Jerky) + fixture faction 50900 land + // The scenario-fixture items (Tempered Blade/Tough Jerky) + fixture faction 50900 land // in fingerprinted catalogue tables (game_item_template/game_faction) — seed them here too, not // only from debug_seed_scenario_fixtures, so every fresh shard agrees regardless of whether the // wire-suite harness ever ran against it (see seed::fixtures::seed_fixture_catalogue's doc). @@ -1025,8 +1025,8 @@ fn seed_spell_registry(ctx: &ReducerContext) { seed_mana_burn_fixture(ctx); // Mana Burn (8129) — E_POWER_BURN drain-mana-into-damage seed_demon_skin_fixture(ctx); // Demon Skin (696 rank 2) — combat-independent health-per-5 tick seed_regen_fixture(ctx); // Test Regeneration (50137) — the combat-regen probe's kind-169 source - // issue #85 audit: this one was previously reachable ONLY via `debug_seed_stealth_fixture` (never - // from init), the same divergence-hazard shape #85 fixed for items/faction — see + // Audit: this one was previously reachable ONLY via `debug_seed_stealth_fixture` (never + // from init), the same divergence-hazard shape fixed for items/faction — see // `seed::fixtures::seed_stealth_fixture`'s doc. seed_stealth_fixture(ctx); // Stealth (1784) — A_STEALTH presence marker seed_mount_fixture(ctx); // Test Riding Horse (50310) + Test Dazed (50311) + the Riding skill data @@ -1208,9 +1208,9 @@ fn seed_spell_registry(ctx: &ReducerContext) { ..base_effect(50118, 1) }); // id 200473 - // (6) Call Stone 50119 (#387) — the Hearthstone's on-use spell: ONE E_RECALL_HOME (0x1F) instant + // (6) Call Stone 50119 — the Hearthstone's on-use spell: ONE E_RECALL_HOME (0x1F) instant // effect, T_SELF, that teleports the caster to its bound home via `world::recall_to_home`. No - // cost/cooldown/cast-time (matches the pre-#387 hardcoded path's IMMEDIATE-teleport behavior — the + // cost/cooldown/cast-time (matches the earlier hardcoded path's IMMEDIATE-teleport behavior — the // vanilla ~10s cast + 1hr CD is a later follow-up, same as Blink's forward-teleport). Wired onto the // Hearthstone template's spellid_1 below (bonding BIND_ON_PICKUP) — `apply_item_use` reads it as // ANY other on-use spell now, with one data-driven exception: `spell_keeps_item` (keyed on THIS @@ -1409,7 +1409,7 @@ fn seed_scheduler_arming(ctx: &ReducerContext) { scheduled_at: ScheduleAt::Interval(TimeDuration::from_micros(500_000)), }); - // Movement republish tick every 50ms = 20 Hz (#461): drains the PRIVATE + // Movement republish tick every 50ms = 20 Hz: drains the PRIVATE // `game_entity_motion_pending` staging table into the public `game_entity_motion` relay in ONE // transaction, so SpacetimeDB's per-transaction subscription sweep runs 20×/s instead of once // per movement packet. LOAD-BEARING — without this row peer movement stages and never relays. @@ -1425,7 +1425,7 @@ fn seed_scheduler_arming(ctx: &ReducerContext) { )), }); - // Gateway lease reaper (#468 stage 4a): despawns the players of a gateway that stopped + // Gateway lease reaper (stage 4a): despawns the players of a gateway that stopped // heartbeating (the shared-connection crash case). Inert while `game_gateway_session` is // empty — nothing binds sessions to leases until stage 4d — but armed from day one so the // ghost bound exists the moment the first leased session appears. Same three-net story as @@ -1678,7 +1678,7 @@ pub(crate) const CREATEINFO_KIT: &[(u8, u8, u32)] = &[ ]; // =========================================================================================== -// #223 — seed idempotence + fixture completeness. +// Seed idempotence + fixture completeness. // // `init` is a `#[reducer(init)]`: it runs ONCE per database and does NOT re-run on an // auto-migrate publish. Everything it seeds is therefore either only-if-empty or an upsert, and @@ -1687,7 +1687,7 @@ pub(crate) const CREATEINFO_KIT: &[(u8, u8, u32)] = &[ // `ReducerContext` harness exists by design), so the DATA // invariants are asserted directly and the two structural ones are pinned by a source scan // through `test_scan::code_of`, which strips comments (a bare `.contains()` on an unstripped body -// is exactly what a trailing-comment needle defeats — issue #64). +// is exactly what a trailing-comment needle defeats). // =========================================================================================== #[cfg(test)] mod tests { @@ -1914,7 +1914,7 @@ mod tests { } } - /// COLLISION SAFETY for the land-mount fixture (issue #22). Every id it reserves has to sit in a + /// COLLISION SAFETY for the land-mount fixture. Every id it reserves has to sit in a /// range the world ETL and the DBC import never write, and none may shadow an existing fixture — /// an id that collides silently replaces real imported data on a live shard, or is silently /// replaced by it, and either way the headless mount scenario stops testing what it claims to. @@ -1969,7 +1969,7 @@ mod tests { /// publish, so a fixture reachable ONLY from `init` never lands on an already-provisioned /// shard, and one reachable only from a `debug_seed_*` reducer never lands on a fresh one /// unless a harness happens to call it. Both halves have gone wrong here: the comments in - /// `init` record `seed_stealth_fixture` having been debug-only (issue #85's audit) and + /// `init` record `seed_stealth_fixture` having been debug-only (the audit) and /// `seed_fixture_catalogue` being moved into `init` for exactly this reason. /// /// So: every fixture seeder must be called from at least one of the two, and the failure names @@ -2009,20 +2009,20 @@ mod tests { } } - /// DRIFT REGRESSION (#363). The post-import fixture-restore path + /// DRIFT REGRESSION. The post-import fixture-restore path /// (`seed_scenario_fixtures`/`seed_fixture_items`, run via `debug_seed_scenario_fixtures` /// after a world-ETL re-import truncates `game_creature_template`/`game_item_template`) used /// to re-author full `CreatureTemplate`/`ItemTemplate` literals as hand-copies of `init`'s — /// and they drifted: the Profession Trainer was level 30/1500hp/"Cooking & Skinning" in /// `init` but level 10/100hp/"Fixture" in the restore copy, and the Test Wolf's /// money_min/max disagreed too. A shard restored after an ETL wipe therefore carried - /// different fixtures than a fresh one — the exact cross-shard divergence class #85 was + /// different fixtures than a fresh one — the exact cross-shard divergence class was /// filed to kill, reintroduced by copy-paste. /// /// The fix collapses both paths onto ONE canonical constructor per fixture /// (`test_wolf_template`, `profession_trainer_template`, `tempered_blade_template`, /// `tough_jerky_template`, all in `seed/fixtures.rs`). This pins that both `init` (via - /// `seed_map0_demo_content`, #377's stratum-2 split-out — see this file's header) and the + /// `seed_map0_demo_content`, the stratum-2 split-out — see this file's header) and the /// restore path call the SAME constructors — a hand-copied struct literal reintroduced in /// either one fails this test loudly instead of silently drifting again. #[test] diff --git a/module/src/seed/fixtures.rs b/module/src/seed/fixtures.rs index 1ee4f35c6..7266d6a5f 100644 --- a/module/src/seed/fixtures.rs +++ b/module/src/seed/fixtures.rs @@ -23,10 +23,10 @@ use crate::{ /// Canonical fixture-NPC/item constructors — the single source of truth for the synthetic rows /// that BOTH `seed::init` (fresh publish) and the post-import fixture-restore path /// (`seed_scenario_fixtures`, invoked by `debug_seed_scenario_fixtures` after a world-ETL -/// re-import truncates `game_creature_template`/`game_item_template`) insert. Before this (#363) +/// re-import truncates `game_creature_template`/`game_item_template`) insert. Before this /// the restore path re-authored these as hand-copied literals that drifted from `init`'s own /// (Profession Trainer: level 30/1500hp/"Cooking & Skinning" vs level 10/100hp/"Fixture"; Test -/// Wolf: money_min/max 0/0 vs 25/50) — the exact cross-shard divergence class #85 was filed to +/// Wolf: money_min/max 0/0 vs 25/50) — the exact cross-shard divergence class was filed to /// kill, reintroduced by copy-paste. Both callers now build from these fns so there is nothing /// left to hand-copy out of sync. `init`'s values are treated as authoritative (they carry the /// original design rationale, preserved below). @@ -389,7 +389,7 @@ pub(crate) fn tempered_blade_template(entry: u32) -> ItemTemplate { /// "Tough Jerky" — hand-authored reference food (licensing firewall: never bulk-imported; /// display 1542 ships in 5875, placeholder icon). `entry` parameterized the same way as -/// `tempered_blade_template`. spellid_1 (#387) points at "Eating" (50115) — the same food HoT the +/// `tempered_blade_template`. spellid_1 points at "Eating" (50115) — the same food HoT the /// real Tough Jerky (117) / Tough Hunk of Bread (4540) items use — so this NO-IMPORT fixture stays /// usable now that `apply_item_use` reads spellid_1 as the single on-use authority. pub(crate) fn tough_jerky_template(entry: u32) -> ItemTemplate { @@ -436,7 +436,7 @@ pub(crate) fn riding_reins_template(entry: u32) -> ItemTemplate { } } -/// Base-row constructors (#377) — every meaningful field of a fixture `Spell`/`SpellEffect`/ +/// Base-row constructors — every meaningful field of a fixture `Spell`/`SpellEffect`/ /// `ItemTemplate` is a struct-update override at the call site; every OTHER field (the ~80% that /// are 0/false/empty on a synthetic fixture row) comes from one of these three fns. Mirrors the /// `seed.rs` `spell`/`effect` closures' own implicit defaults exactly (so a fixture built this way @@ -759,8 +759,8 @@ pub(crate) fn seed_mana_burn_fixture(ctx: &ReducerContext) { /// (never timer-reaped; see `spell::taxonomy::A_STEALTH` / `scheduler.rs`'s reap-skip). IDEMPOTENT /// (inserts only if absent). /// -/// Issue #85 audit: until this call was wired into `init` below, this fixture was reachable ONLY via -/// `debug_seed_stealth_fixture` — the exact bug class #85 fixed for the item/faction fixtures, just +/// Audit: until this call was wired into `init` below, this fixture was reachable ONLY via +/// `debug_seed_stealth_fixture` — the exact bug class fixed for the item/faction fixtures, just /// for `game_spell`/`game_spell_effect` (the `spells` catalogue-fingerprint family) instead. It was /// masked live only because 1784 is a REAL vanilla id the Spell.dbc importer already seeds on every /// shard that has imported, so the insert-if-absent guards below silently no-op post-import — but a @@ -1116,12 +1116,12 @@ pub(crate) fn seed_regen_fixture(ctx: &ReducerContext) { /// a production build. pub(crate) const FIXTURE_BLADE: u32 = 5090050; pub(crate) const FIXTURE_JERKY: u32 = 5090052; -/// The reserved reusable MOUNT item (issue #22) — same reserved-entry rationale as the two above. +/// The reserved reusable MOUNT item — same reserved-entry rationale as the two above. pub(crate) const FIXTURE_REINS: u32 = 5090054; /// Insert the two reserved fixture item templates (insert-if-absent) — built from the same /// `tempered_blade_template`/`tough_jerky_template` constructors the mock-seed's Tempered Blade -/// (50) / Tough Jerky (52) use, under the reserved entries above (#363: this used to be a +/// (50) / Tough Jerky (52) use, under the reserved entries above (this used to be a /// hand-copied literal that could drift from the mock-seed's). fn seed_fixture_items(ctx: &ReducerContext) { let items = ctx.db.game_item_template(); @@ -1136,7 +1136,7 @@ fn seed_fixture_items(ctx: &ReducerContext) { } } -// --- LAND-MOUNT FIXTURE (issue #22) ------------------------------------------------------------- +// --- LAND-MOUNT FIXTURE ------------------------------------------------------------- // Reserved ids so a headless sandbox can ride, dismount and fail every mount gate with no Spell.dbc. // A real imported world reaches the same behaviour through taxonomy + skill data alone; nothing here // is referenced by runtime code. @@ -1288,9 +1288,9 @@ pub(crate) const FIXTURE_FACTION: u32 = 50900; /// Seed the reserved-id CATALOGUE rows the scenario fixtures reference: the two fixture items /// (`FIXTURE_BLADE`/`FIXTURE_JERKY`) and the fixture faction above. Split out from -/// `seed_scenario_fixtures` (issue #85) and called from `init` too — same precedent as +/// `seed_scenario_fixtures` and called from `init` too — same precedent as /// `seed_pw_shield_fixture` — because these rows land in tables the cross-shard catalogue parity -/// check (#82) fingerprints whole (`game_item_template`, `game_faction`): before this, only a shard +/// check fingerprints whole (`game_item_template`, `game_faction`): before this, only a shard /// that had `debug_seed_scenario_fixtures` run against it (historically the wire-suite's target, /// lyracore) carried them, so its `items`/`dbc_reference` fingerprints permanently disagreed /// with siblings that never ran the harness reducer — a false catalogue-skew signal, not a real one. @@ -1432,7 +1432,7 @@ pub(crate) fn seed_scenario_fixtures(ctx: &ReducerContext) { // dump — the INIT-seeded fixture templates (Test Wolf 51000, Profession Trainer 51001) vanish // on every re-import, breaking the wire scenarios until someone reseeds by hand. Re-seed them // HERE (this reducer is the operator's idempotent post-import fixture restore) from the SAME - // canonical constructors `seed::init` uses (#363: this used to be a hand-copied literal that + // canonical constructors `seed::init` uses (this used to be a hand-copied literal that // drifted — Profession Trainer was level 10/100hp/"Fixture" here vs level 30/1500hp/"Cooking & // Skinning" in init, and Test Wolf's money_min/max disagreed too). let templates = ctx.db.game_creature_template(); @@ -1684,7 +1684,7 @@ pub(crate) fn seed_scenario_fixtures(ctx: &ReducerContext) { }); } - // --- RIDING TRAINER (issue #22): the `trainer_type::MOUNTS` NPC that teaches the Riding skill line + // --- RIDING TRAINER: the `trainer_type::MOUNTS` NPC that teaches the Riding skill line // at its two vanilla tiers. Same offering shape as the weapon master, but `learn_skill_line` names // Riding, so `apply_trainer_buy` takes the riding fork (grant whole at the offering's tier) and // refuses the row on any trainer that is not a MOUNTS trainer. @@ -1740,7 +1740,7 @@ pub(crate) fn seed_scenario_fixtures(ctx: &ReducerContext) { /// re-imported kit + a fixture re-seed collided live 2026-07-15. Delete-then-insert keeps the /// fixture authoritative for its own rows without tripping the constraint. /// -/// THE DISCIPLINE (#377): delete-then-insert is idempotent BY CONSTRUCTION — calling it twice with +/// THE DISCIPLINE: delete-then-insert is idempotent BY CONSTRUCTION — calling it twice with /// the same row is a no-op, and calling it with a changed shape self-corrects the row in place. So /// every call site below calls this UNCONDITIONALLY, with no `if find(id).is_none() { ... }` guard /// around it. A guard doesn't just add noise: it makes the delete dead code (the branch that would diff --git a/module/src/skill.rs b/module/src/skill.rs index aff41a86d..f31ba9743 100644 --- a/module/src/skill.rs +++ b/module/src/skill.rs @@ -343,7 +343,7 @@ crate::character_owned!(delete, fn sweep_delete_game_player_skill(ctx, character skills.id().delete(r.id); } }); -// CROSS-DATABASE transport (issue #19), HOT: weapon/defense/profession skill values gate every +// CROSS-DATABASE transport, HOT: weapon/defense/profession skill values gate every // swing the character makes on arrival. `id` is a surrogate PK, re-minted. crate::character_owned!(transfer, fn sweep_transfer_game_player_skill(ctx, character_guid, io) { table = game_player_skill, @@ -1159,7 +1159,7 @@ pub fn debug_set_skill( #[cfg(feature = "debug_reducers")] #[spacetimedb::reducer] pub fn debug_reseed_skills(ctx: &ReducerContext, character_guid: u64) -> Result<(), String> { - // REFUSE verdict (issue #30): `game_player_skill` is a HOT manifest table, so a post-begin + // REFUSE verdict: `game_player_skill` is a HOT manifest table, so a post-begin // reseed is a lost write cross-database. let ch = crate::helpers::require_character(ctx, character_guid) .map_err(|_| format!("no game_character row for guid {character_guid}"))?; diff --git a/module/src/spell/cast/resolve.rs b/module/src/spell/cast/resolve.rs index 9ecaa42d4..2010fc660 100644 --- a/module/src/spell/cast/resolve.rs +++ b/module/src/spell/cast/resolve.rs @@ -959,7 +959,7 @@ fn check_cast_gate_suffix( } } - // Linked-debuff refusal gate (Weakened Soul / "diminishing re-application" mechanic, #013): an aura + // Linked-debuff refusal gate (Weakened Soul / "diminishing re-application" mechanic): an aura // effect may carry a LINKED debuff spell id in its otherwise-dead `p1` field (every current aura effect // imports/seeds `p1 == 0` — see `aura_apply`'s `eff_p1` freeze, which only special-cases // `A_PERIODIC_TRIGGER`; this is the first consumer of a nonzero `p1` on any OTHER aura kind). When set, @@ -1304,7 +1304,7 @@ fn begin_cast_with_admission( /// effect, `E_PERSISTENT_AREA` — a ground-AoE) has no business being handed to `aura_apply`: that fn does /// not interpret instant-kind semantics, it just inserts a `game_aura` row verbatim, which manufactures a /// spurious aura — and buff-bar/floating-combat-text feedback — for a spell the target never actually cast -/// (#90: every talent-granted ACTIVE did this on login and world-change). Pure so it is unit-tested +/// (every talent-granted ACTIVE did this on login and world-change). Pure so it is unit-tested /// without a `ReducerContext` (module crate convention — no ctx harness by design). pub(crate) fn passive_applies_effect_kind(kind: u8) -> bool { kind & KIND_AURA_BIT != 0 @@ -1321,7 +1321,7 @@ pub(crate) fn passive_applies_effect_kind(kind: u8) -> bool { /// `E_PERSISTENT_AREA` effect, no aura effects) must light nothing here; `learn_spell` (the caller in /// `talent.rs`) already puts the spell in the book, which is what makes it castable. Mirrors the same /// `KIND_AURA_BIT` filter the real cast path already applies (`apply_effect`/`apply_linked_debuff` above). -/// #90: this filter was previously missing, so EVERY instant effect of a talent rank spell (e.g. +/// This filter was previously missing, so EVERY instant effect of a talent rank spell (e.g. /// Consecration's ground-AoE) was handed to `aura_apply`, minting a spurious aura row / floating combat /// text on every login and world change. [entity] pub(crate) fn apply_spell_auras( diff --git a/module/src/spell/control.rs b/module/src/spell/control.rs index a5bcd0dca..4e86187ad 100644 --- a/module/src/spell/control.rs +++ b/module/src/spell/control.rs @@ -373,7 +373,7 @@ pub(crate) fn pushback_cast(ctx: &ReducerContext, caster_guid: u64) { } /// Cancel `caster_guid`'s in-progress (timed) cast(s) — delete its `game_pending_cast` row(s), which also -/// cancels the scheduled `fire_pending_cast`, so the spell never resolves (rank #118 CC interrupt). +/// cancels the scheduled `fire_pending_cast`, so the spell never resolves (rank CC interrupt). /// Reached through the `by_caster` index (perf catalog 1.18) — `break_channel` calls this on every /// moving heartbeat, so the old full scan sat on the hottest path. Returns whether anything was interrupted; a caster with no pending cast → `false` (no-op). The /// client-facing cast-bar cancel (an `SMSG_SPELL_FAILED`/interrupted wire push) is a gateway follow-up — diff --git a/module/src/spell/effects.rs b/module/src/spell/effects.rs index 2b027c7d1..f05493a5a 100644 --- a/module/src/spell/effects.rs +++ b/module/src/spell/effects.rs @@ -88,7 +88,7 @@ pub(crate) fn apply_target_damage( { return (0, 0); } - // Stage 1 of the SHARED damage pipeline (#370): the caster's outgoing % (Defensive Stance's −10% + // Stage 1 of the SHARED damage pipeline: the caster's outgoing % (Defensive Stance's −10% // plus any A_MOD_COMBAT(COMBAT_DMG_DONE) curse), the target's A_MOD_DAMAGE_TAKEN %, absorb shields // (soaked BEFORE the kill check, so a fully-absorbed hit can't kill), and the GM godmode zero. // This is the exact chain the melee/ranged swings fold — they used to be four hand-maintained @@ -255,7 +255,7 @@ pub(crate) fn recompute_vitals(ctx: &ReducerContext, unit_guid: u64) { entities.guid().update(e); } -/// Recompute `unit_guid`'s CHARACTER-SHEET numbers (#517) — the paperdoll fields the gateway's +/// Recompute `unit_guid`'s CHARACTER-SHEET numbers — the paperdoll fields the gateway's /// `build_sheet_stats_values` merely reads back, never computes: this is the SINGLE fold, so no aura or /// gear rule is ever duplicated gateway-side (the trap the previous gateway-only `sheet_stats` mirror /// hit — it never read `game_aura`, so no aura, including Battle Shout, could ever move the sheet). @@ -306,7 +306,7 @@ pub(crate) fn recompute_sheet(ctx: &ReducerContext, unit_guid: u64) { e.level, crate::combat::equipped_ranged_weapon(ctx, unit_guid), ); - // #532: melee crit for the sheet is a plain READ of the same fold the swing table rolls + // Melee crit for the sheet is a plain READ of the same fold the swing table rolls // against — never a second crit formula. let crit_bp = crate::combat::effective_crit_bp(ctx, &e); diff --git a/module/src/spell/math.rs b/module/src/spell/math.rs index 0e7025382..9cfd4308b 100644 --- a/module/src/spell/math.rs +++ b/module/src/spell/math.rs @@ -765,7 +765,7 @@ pub(crate) fn aura_moves_vitals(kind: u8, p0: i32) -> bool { /// True iff `effect` is an aura that moves a character-sheet number `recompute_sheet` derives: any /// `A_MOD_STAT` (all five attributes, including `STAT_ALL`), an `A_MOD_COMBAT(COMBAT_ATTACK_POWER)` -/// aura (Battle Shout), or an `A_MOD_COMBAT(COMBAT_CRIT)` aura (#532 — `sheet_crit_bp`, the +/// aura (Battle Shout), or an `A_MOD_COMBAT(COMBAT_CRIT)` aura (`sheet_crit_bp`, the /// `PLAYER_CRIT_PERCENTAGE` source). Strictly wider than [`aura_moves_vitals`] (which only cares about /// the two stats that move a pool) — a +STR/+AGI/+SPI buff, an AP buff, or a crit-rating buff moves the /// sheet even though none of them move a pool. Pure → unit-tested (mirrors `aura_moves_vitals`'s test diff --git a/module/src/spell/scheduler.rs b/module/src/spell/scheduler.rs index a2588f347..9b3763cbe 100644 --- a/module/src/spell/scheduler.rs +++ b/module/src/spell/scheduler.rs @@ -110,7 +110,7 @@ pub fn fire_spell_impact(ctx: &ReducerContext, sched: PendingSpellImpact) { }); } -/// The spellbook-gated cast core, actor-explicit (#479): everything [`cast_spell`] does after +/// The spellbook-gated cast core, actor-explicit: everything [`cast_spell`] does after /// resolving WHO is casting — the sender reducer above and `gw::gw_cast_spell` both delegate here. /// Distinct from [`crate::actor::cast_at`], which drives `resolve_cast_at` and skips the spellbook /// gate; this is the player-shaped entry (cast-time aware, self-cast on `target_guid == 0`). @@ -143,7 +143,7 @@ pub(crate) fn do_cast_spell( ) } -/// The shared core behind [`cast_spell_at`] and its gateway twin `gw_cast_at` (#479) — the +/// The shared core behind [`cast_spell_at`] and its gateway twin `gw_cast_at` — the /// ground-target cast with the spellbook, NaN and self-target-default gates. pub(crate) fn apply_cast_spell_at( ctx: &ReducerContext, @@ -216,7 +216,7 @@ pub(crate) fn do_resurrect_response( Ok(()) } -/// The cancel-cast core, actor-explicit (#479) — same split as [`do_cast_spell`]. +/// The cancel-cast core, actor-explicit — same split as [`do_cast_spell`]. pub(crate) fn do_cancel_cast( ctx: &ReducerContext, caster: crate::WorldEntity, @@ -234,7 +234,7 @@ pub(crate) fn do_cancel_cast( Ok(()) } -/// The cancel-aura core, actor-explicit (#479) — same split as [`do_cast_spell`], keyed by guid +/// The cancel-aura core, actor-explicit — same split as [`do_cast_spell`], keyed by guid /// (the body never reads another column off the row). pub(crate) fn do_cancel_aura( ctx: &ReducerContext, @@ -291,7 +291,7 @@ pub(crate) fn do_cancel_aura( if revitalize { recompute_vitals(ctx, player_guid); } - // #517: same wider gate as the apply site (targeting.rs) — AFTER vitals so a lost STA/INT aura's + // Same wider gate as the apply site (targeting.rs) — AFTER vitals so a lost STA/INT aura's // sheet recompute sees the already-updated `e.spirit`. if resheet { recompute_sheet(ctx, player_guid); @@ -646,7 +646,7 @@ pub fn tick_auras(ctx: &ReducerContext, _schedule: AuraSchedule) { auras.count() ); let mut revitalize: Vec = Vec::new(); - // #517: same expiry snapshot, wider gate — a reaped Battle Shout (or a STR/AGI/SPI stat aura) must + // Same expiry snapshot, wider gate — a reaped Battle Shout (or a STR/AGI/SPI stat aura) must // pull the sheet numbers back down too, even though it never touched a vitals pool. let mut resheet: Vec = Vec::new(); // A mount aura reaping on its own timer must leave the same end state as right-clicking the buff @@ -868,7 +868,7 @@ pub fn tick_ground_areas(ctx: &ReducerContext, _schedule: GroundAreaSchedule) { /// Ensure the 500ms `tick_ground_areas` schedule row exists — called wherever a `game_ground_area` /// row is BORN (perf catalog 1.20's disarm deletes it whenever the area table drains). Idempotent and /// cheap: the table holds 0 or 1 rows. Must stay in lockstep with the interval in `seed::init` / -/// `debug_repair_after_publish` (#378, formerly the standalone `debug_ensure_ground_area_schedule`). +/// `debug_repair_after_publish` (formerly the standalone `debug_ensure_ground_area_schedule`). pub(crate) fn arm_ground_area_tick(ctx: &ReducerContext) { if ctx.db.game_ground_area_schedule().count() > 0 { return; diff --git a/module/src/spell/spellbook.rs b/module/src/spell/spellbook.rs index c2fd396b3..4c2390837 100644 --- a/module/src/spell/spellbook.rs +++ b/module/src/spell/spellbook.rs @@ -31,7 +31,7 @@ crate::character_owned!(delete, fn sweep_delete_game_player_spell(ctx, character spells.id().delete(r.id); } }); -// CROSS-DATABASE transport (issue #19), HOT: the spellbook is what the character can DO on arrival — +// CROSS-DATABASE transport, HOT: the spellbook is what the character can DO on arrival — // without it the login spellbook is empty and every cast fails "spell not known". `id` is a // surrogate PK, re-minted. crate::character_owned!(transfer, fn sweep_transfer_game_player_spell(ctx, character_guid, io) { diff --git a/module/src/spell/stacking.rs b/module/src/spell/stacking.rs index 8d897a418..4d48fe447 100644 --- a/module/src/spell/stacking.rs +++ b/module/src/spell/stacking.rs @@ -98,21 +98,13 @@ pub struct DrState { /// cross-checking a category assignment against public documentation cheap) — NOT the same numbering /// as this crate's own `M_STUN`/`M_ROOT`/`M_FEAR`/`M_POLY` mechanic consts in `taxonomy.rs`, hence the /// separate mapping fn below rather than reusing the mechanic value directly). Only stun/fear/root/poly are -/// reachable today (the only mechanics we have — `M_CHARM`/`M_SLEEP`/`M_DISORIENT` don't exist yet); -/// charm/sleep/disorient are declared for completeness and to extend cleanly as those mechanics import. +/// reachable today — they are the only mechanics we have. +// 3, 5 and 7 are left unnamed: charm/sleep/disorient have no mechanic in `taxonomy.rs` yet, so +// naming them now would be a constant with no reader. pub(crate) const DR_CAT_STUN: u8 = 1; pub(crate) const DR_CAT_FEAR: u8 = 2; -// Reserved: no mechanic maps to these yet (`M_CHARM`/`M_SLEEP`/`M_DISORIENT` don't exist in `taxonomy.rs` -// today) — declared now so `dr_category_for_mechanic` extends by adding ONE match arm when they import, -// never a renumbering. `#[allow(dead_code)]` documents that as deliberate. -#[allow(dead_code)] -pub(crate) const DR_CAT_CHARM: u8 = 3; pub(crate) const DR_CAT_ROOT: u8 = 4; -#[allow(dead_code)] -pub(crate) const DR_CAT_SLEEP: u8 = 5; pub(crate) const DR_CAT_POLY: u8 = 6; -#[allow(dead_code)] -pub(crate) const DR_CAT_DISORIENT: u8 = 7; /// 15 seconds, in micros — the DR window length, measured from the CC aura's REMOVAL (not its apply). pub(crate) const DR_WINDOW_MICROS: i64 = 15_000_000; diff --git a/module/src/spell/tables.rs b/module/src/spell/tables.rs index fe9cc661e..97fffb2ce 100644 --- a/module/src/spell/tables.rs +++ b/module/src/spell/tables.rs @@ -274,7 +274,7 @@ pub struct Aura { pub proc_ready_micros: i64, } -// UNIT-keyed character-owned sweep (issue #72, the warm-handoff hot-state audit). `Aura`'s columns +// UNIT-keyed character-owned sweep (the warm-handoff hot-state audit). `Aura`'s columns // name ANY unit (`target_guid`/`caster_guid` — creatures have auras too), never `character_guid`/ // `player_guid`/`owner_guid`, so `tripwires.rs`'s `character_owned_tripwire` never flags this table and no // marker was mandatory. That silence is exactly why the TRANSFER half went missing while the DELETE @@ -291,7 +291,7 @@ crate::character_owned!(delete, fn sweep_delete_game_aura(ctx, character_guid) { auras.id().delete(id); } }); -// CROSS-DATABASE transport (issue #19/#72): `id` is `#[auto_inc]` — meaningless, and possibly +// CROSS-DATABASE transport: `id` is `#[auto_inc]` — meaningless, and possibly // COLLIDING, on the destination — reset to 0 so the insert mints a fresh one, mirroring // `sweep_transfer_game_character_talent`'s identical re-mint of its own surrogate PK. Every OTHER // column, including `applied_at`/`expires_at`, rides unchanged: both are ABSOLUTE `Timestamp`s (a diff --git a/module/src/spell/taxonomy.rs b/module/src/spell/taxonomy.rs index 13cfa32a8..fc06269f4 100644 --- a/module/src/spell/taxonomy.rs +++ b/module/src/spell/taxonomy.rs @@ -107,7 +107,7 @@ pub(crate) const E_PERSISTENT_AREA: u8 = 0x1B; // GROUND-AoE (118, Consecration/ pub(crate) const E_FISH: u8 = 0x1C; // Fishing (060): gateway-intercepted like E_ENCHANT_ITEM/E_DISENCHANT — CMSG_CAST_SPELL for a spell carrying this kind routes to the `fish` reducer (instant-resolve alpha catch; the bobber/channel flow is the deferred follow-up). Inert in-module (no resolve arm); exists so the gateway routes by DATA, never a spell-id list. pub(crate) const E_OPEN_LOCK: u8 = 0x1D; // Pick Lock (119): gateway-intercepted like E_FISH (0x1C) — CMSG_CAST_SPELL for a spell carrying this kind routes to the `pick_lock` reducer (unlock a locked GameObject, gated on the caster's Lockpicking 633 skill vs the game_lock required_skill). Inert in-module (NO resolve arm in cast.rs); exists so the gateway routes by DATA, never a spell-id list. 0x1E is reserved for a future E_SUMMON_PORTAL — do NOT reuse. pub(crate) const E_BLINK: u8 = 0x1A; // teleport the caster ~20yd FORWARD along its facing (Mage Blink, 116): a self-cast position change reusing the teleport core (like E_CHARGE), clamped to the furthest nav-LoS-clear point so it doesn't cross geometry. Root/snare removal rides a separate A_IMMUNITY effect. The importer name-rescues the dead SCRIPT teleport effect (raw 29) to this kind -pub(crate) const E_RECALL_HOME: u8 = 0x1F; // teleport the caster to its bound HOME (Hearthstone, #387): a self-cast recall reusing the shared `world::recall_to_home` core, always to instance 0 regardless of the caster's current instance. Data-driven — a consumable's `spellid_1` naming a spell that carries this kind IS "a recall item"; `items::ops::apply_item_use` reads that (not a hardcoded item entry) to skip the normal stack-consumption a used-up consumable takes, since a recall trinket is never consumed. No cost/cooldown gate yet (the vanilla ~10s cast + 1hr CD is the same later follow-up E_BLINK's forward-teleport already defers) +pub(crate) const E_RECALL_HOME: u8 = 0x1F; // teleport the caster to its bound HOME (Hearthstone): a self-cast recall reusing the shared `world::recall_to_home` core, always to instance 0 regardless of the caster's current instance. Data-driven — a consumable's `spellid_1` naming a spell that carries this kind IS "a recall item"; `items::ops::apply_item_use` reads that (not a hardcoded item entry) to skip the normal stack-consumption a used-up consumable takes, since a recall trinket is never consumed. No cost/cooldown gate yet (the vanilla ~10s cast + 1hr CD is the same later follow-up E_BLINK's forward-teleport already defers) pub(crate) const E_DUEL: u8 = 0x22; // Duel (raw effect 83): request a server-authoritative Duel; p0 is the duel-flag gameobject template entry /// Remove the target's active LAND MOUNT (Dazed's mount-removal half). An instant effect that calls the /// one shared `mount::dismount` — idempotent, a silent no-op on an unmounted target, and never touched by @@ -368,7 +368,7 @@ pub(crate) const RESIST_ARMOR: u8 = 0x01; // physical armor (bit 0 of the school /// that exists": `tests.rs`'s `instant_kind_wire_values_exhaustive` loops it (never a hand-copied /// duplicate) so a kind is exhaustively covered by construction, and referencing every `E_*` const here /// keeps an as-yet-unwired one from tripping `dead_code` (CI's clippy gate runs `-D warnings`, so a -/// kind added to the taxonomy but left OFF this slice fails the build, not just a test). #367: this +/// kind added to the taxonomy but left OFF this slice fails the build, not just a test). This /// replaces the old `_TAXONOMY` scaffold + four separately hand-copied `E_*`/`A_*` lists in `tests.rs`, /// one of which had drifted (E_BLINK/E_PERSISTENT_AREA/E_FISH/E_OPEN_LOCK were missing from it). /// `#[allow(dead_code)]`: like `_TAXONOMY` before it, this binding is read only from `#[cfg(test)]` @@ -412,7 +412,7 @@ pub(crate) const ALL_INSTANT_KINDS: &[u8] = &[ E_DISMOUNT, ]; -/// Canonical, ordered list of every AURA (`A_*`) kind — same rationale and same #367 fix as +/// Canonical, ordered list of every AURA (`A_*`) kind — same rationale and same fix as /// [`ALL_INSTANT_KINDS`] above (that older hand-copied list had drifted too, missing /// A_SPELLMOD_FLAT/A_SPELLMOD_PCT). Same `#[allow(dead_code)]` rationale as `ALL_INSTANT_KINDS` above. #[allow(dead_code)] diff --git a/module/src/spell/tests.rs b/module/src/spell/tests.rs index 02f4e170a..a78afb546 100644 --- a/module/src/spell/tests.rs +++ b/module/src/spell/tests.rs @@ -487,10 +487,10 @@ fn kind_wire_values() { /// `Effect` ids onto): a drift here would silently re-route the importer's dispatch and desync it from /// the seed data / the SDK binding. Loops [`ALL_INSTANT_KINDS`] — taxonomy.rs's single canonical list — /// rather than a second hand-copied array, so a kind is covered here purely by being IN that slice -/// (exhaustive by construction: #367 found this test's own hand-copied predecessor missing +/// (exhaustive by construction: the canonical list found this test's own hand-copied predecessor missing /// E_BLINK/E_PERSISTENT_AREA/E_FISH/E_OPEN_LOCK, and a kind left off `ALL_INSTANT_KINDS` itself now /// fails CI's `-D warnings` clippy gate via `dead_code` instead of silently passing every test). Also -/// doubles as the #90 passive-filter regression pin: `passive_applies_effect_kind` is exactly `kind & +/// doubles as the passive-filter regression pin: `passive_applies_effect_kind` is exactly `kind & /// KIND_AURA_BIT != 0`, so asserting every instant kind clears the bit is asserting every instant kind /// is rejected by the passive-apply path (the old, separately hand-copied /// `apply_spell_auras_rejects_instant_effects` test — now redundant with this loop and retired). @@ -523,11 +523,11 @@ fn instant_kind_wire_values_exhaustive() { } /// Pin EVERY `A_*` aura kind wire value: same rationale, same [`ALL_AURA_KINDS`]-loop construction, and -/// same #90-filter double duty as [`instant_kind_wire_values_exhaustive`] above (that test's old sibling +/// same-filter double duty as [`instant_kind_wire_values_exhaustive`] above (that test's old sibling /// hand-copied list was missing A_SPELLMOD_FLAT/A_SPELLMOD_PCT). This is the control: a genuinely /// passive talent/racial (an `A_*` aura-kind effect) must keep applying through `apply_spell_auras` — an /// over-eager filter that rejected real auras too would silently disable every passive talent in the -/// game, worse than the cosmetic bug #90 fixes (the old, separately hand-copied +/// game, worse than the cosmetic bug the passive filter fixes (the old, separately hand-copied /// `apply_spell_auras_still_applies_aura_effects` test — now redundant with this loop and retired). #[test] fn aura_kind_wire_values_exhaustive() { @@ -560,15 +560,15 @@ fn aura_kind_wire_values_exhaustive() { /// `apply_spell_auras_still_applies_aura_effects` tests both called `passive_applies_effect_kind` /// DIRECTLY — they pinned the predicate's logic but never touched `apply_spell_auras` itself. A /// reviewer deleted the `.filter(|e| passive_applies_effect_kind(e.kind))` call from -/// `apply_spell_auras`'s effect query (the #90 fix's actual production wiring) and the full test suite +/// `apply_spell_auras`'s effect query (the fix's actual production wiring) and the full test suite /// stayed green, because nothing exercised the call site. This is a source-scan tripwire on that WIRING: /// it catches the filter call being DELETED (the shape this defect took, confirmed by mutating it and /// watching this test go red) or the query losing the `.filter(` step entirely. It does NOT catch the /// filter being wired to a DIFFERENT, wrong predicate at the same call site (`.filter(|e| /// some_other_fn(e.kind))` still reads as "filtered" to a scan) — the two kind-exhaustive tests above /// are what pin the LOGIC; this one only pins that SOME call to `passive_applies_effect_kind` still -/// gates the query. Uses the crate-shared scan primitives (`crate::test_scan`, #64) rather than a local -/// copy — #367 found this file's own copy was the seventh, and it carried the weaker (non-string- +/// gates the query. Uses the crate-shared scan primitives (`crate::test_scan`) rather than a local +/// copy — found this file's own copy was the seventh, and it carried the weaker (non-string- /// literal-aware) trailing-comment stripper the canonical one was hardened against. #[test] fn apply_spell_auras_still_calls_the_passive_effect_filter() { @@ -702,7 +702,7 @@ fn aura_moves_vitals_gate() { /// The sheet-recompute gate: wider than `aura_moves_vitals` — every `A_MOD_STAT` attribute (including /// STR/AGI/SPI, which are inert for vitals) trips it, and so does an `A_MOD_COMBAT(COMBAT_ATTACK_POWER)` -/// aura (Battle Shout) or an `A_MOD_COMBAT(COMBAT_CRIT)` aura (#532 — a crit-rating buff like the test +/// aura (Battle Shout) or an `A_MOD_COMBAT(COMBAT_CRIT)` aura (a crit-rating buff like the test /// "Combat Insight") even though neither kind moves a pool. #[test] fn aura_moves_sheet_gate() { @@ -782,7 +782,7 @@ fn is_due_for_expiry_permanent_sentinel_never_reaped() { assert!(!is_due_for_expiry(A_STEALTH, permanent, far_future_now)); } -// --- Consumable on-use magnitudes (moved from items::ops, #387 "smalls": these exercise ONLY +// --- Consumable on-use magnitudes (moved from items::ops, "smalls": these exercise ONLY // crate::spell functions against hand-copied seed magnitudes, not anything item-specific) -------------- /// Potion heal CLAMPS to max health — the potion routes through `E_HEAL`→`apply_heal`→`healed_value`, diff --git a/module/src/stats.rs b/module/src/stats.rs index 417df8624..97b7aef7c 100644 --- a/module/src/stats.rs +++ b/module/src/stats.rs @@ -185,7 +185,7 @@ pub(crate) struct LevelStatsDelta { /// This was three hand-mirrored copies (`build_player_entity` at login, `grant_xp`'s ding loop, and /// `set_character_level`) held in sync only by a comment ("mirrors build_player_entity"). The third /// one drifted and dropped the armor line, so a level-SET character kept stale armor until its next -/// relog (#362). One function now, called from all three sites — the drift class is gone structurally, +/// relog. One function now, called from all three sites — the drift class is gone structurally, /// not just patched at the one site that happened to be caught. pub(crate) fn apply_level_stats( ctx: &ReducerContext, @@ -259,7 +259,7 @@ pub(crate) fn set_character_level( e.health = e.max_health; e.power = e.max_power; // refill power to the new max entities.guid().update(e); - // Sheet AP/damage-range are level-derived (#517) and only ever move via `recompute_sheet` — + // Sheet AP/damage-range are level-derived and only ever move via `recompute_sheet` — // without this a GM `.level` set leaves the paperdoll's AP/damage numbers frozen at the // pre-set level until an unrelated trigger (aura/gear/relog) fires. crate::spell::recompute_sheet(ctx, character_guid); @@ -319,14 +319,14 @@ mod tests { assert!(crate::xp::xp_to_next_level(10) > crate::xp::xp_to_next_level(1)); } - /// #362: the level-derived stat block (attributes, armor, max health/power) used to be + /// The level-derived stat block (attributes, armor, max health/power) used to be /// hand-mirrored at three call sites — `build_player_entity` (login), `grant_xp`'s ding loop, and /// `set_character_level` — kept in sync only by a comment ("mirrors build_player_entity"). The /// third one drifted and dropped the armor recompute, so a level-SET character kept stale armor /// until its next relog. The fix extracts ONE `apply_level_stats` writer; this pins that (a) the /// writer still recomputes armor, and (b) all three sites still route through it rather than /// re-inlining the curve lookups — a fresh hand-rolled copy at any of them reintroduces the exact - /// drift class #362 was filed for. + /// drift class was filed for. #[test] fn the_level_recompute_writer_and_all_three_call_sites_stay_wired_together() { let writer_body = diff --git a/module/src/talent.rs b/module/src/talent.rs index 44bcd6da5..3335bc9b6 100644 --- a/module/src/talent.rs +++ b/module/src/talent.rs @@ -139,7 +139,7 @@ crate::character_owned!(delete, fn sweep_delete_game_character_talent(ctx, chara talents.id().delete(r.id); } }); -// CROSS-DATABASE transport (issue #19): spent talent ranks are durable progression — a character +// CROSS-DATABASE transport: spent talent ranks are durable progression — a character // arriving without them is a respec nobody asked for. `id` is a surrogate PK, re-minted. crate::character_owned!(transfer, fn sweep_transfer_game_character_talent(ctx, character_guid, io) { table = game_character_talent, @@ -455,7 +455,6 @@ pub fn apply_learned_talents(ctx: &ReducerContext, guid: u64, owner: Identity, l // Respec — unlearn every learned talent for an escalating gold cost (work-item 198) // =========================================================================================== -#[allow(dead_code)] // core kept for a future gw_reset_talents twin (#483 deleted the sender-path reducer) /// Gold cost (copper) of a character's NEXT respec, given `respec_count` prior resets: vanilla's /// `Player::resetTalentsCost` step table — 1g / 5g / 10g, then +5g per further reset, capped at 50g /// (1.12 has no cost decay — that's a TBC addition). Expressed directly in copper (1g = 10_000c, the @@ -471,8 +470,8 @@ pub(crate) fn respec_cost_copper(respec_count: u32) -> u32 { copper.min(CAP_COPPER) } -#[allow(dead_code)] // core kept for a future gw_reset_talents twin (#483 deleted the sender-path reducer) -/// Shared validated core of a talent respec (the player + debug reducers both call this). Gates +/// Shared validated core of a talent respec (`gw_reset_talents` and the debug reducer both call +/// this). Gates /// EXACTLY like `trainer::apply_trainer_buy` (a real in-range TRAINER on the same map), then charges /// `respec_cost_copper(character.respec_count)` and unwinds every learned talent: the passive auras /// those talents applied (deleted directly — `cancel_aura` refuses a PASSIVE spell) and the abilities @@ -489,7 +488,7 @@ pub(crate) fn do_reset_talents( .guid() .find(character_guid) .ok_or_else(|| format!("no live entity for guid {character_guid}"))?; - // Same gates as `apply_trainer_buy` (issue #372's shared validate_trainer_interaction): a real + // Same gates as `apply_trainer_buy` (the shared validate_trainer_interaction): a real // TRAINER, on the caller's map, within interaction range. crate::trainer::validate_trainer_interaction(ctx, &entity, trainer_guid)?; diff --git a/module/src/terrain.rs b/module/src/terrain.rs index 2cd18007e..56d49f603 100644 --- a/module/src/terrain.rs +++ b/module/src/terrain.rs @@ -52,7 +52,7 @@ pub fn ground_z(ctx: &ReducerContext, map_id: u32, x: f32, y: f32) -> Option Result<(), Stri /// callers skip zone-scoping entirely rather than guessing wrong — a wrong guess would silently narrow /// a candidate set (graveyards, fishing loot) to the WRONG zone. /// -/// The single canonical zone resolver (#375, work-item 209 idiom): `world::graveyard`'s release pick +/// The single canonical zone resolver (work-item 209 idiom): `world::graveyard`'s release pick /// and `loot::apply_fish`'s catch roll both call this instead of each keeping its own one-hop walk. pub fn zone_id_at(ctx: &ReducerContext, map_id: u32, x: f32, y: f32) -> Option { area_at(ctx, map_id, x, y).map(|area| zone_of(&area)) diff --git a/module/src/test_scan.rs b/module/src/test_scan.rs index a2ba5bc56..71fe6f9d3 100644 --- a/module/src/test_scan.rs +++ b/module/src/test_scan.rs @@ -4,7 +4,7 @@ //! into a pure function and asserted on directly instead, wherever that is reachable — see //! `helpers::gate_in_transit` for the pattern). //! -//! Issue #64: this used to be six near-identical copies (`auth.rs`, `chat.rs`, `group.rs`, +//! This used to be six near-identical copies (`auth.rs`, `chat.rs`, `group.rs`, //! `instance.rs`, `world.rs`, `transfer.rs`), and they had already drifted — two of them stripped a //! `// trailing comment` off an otherwise-live line, the other four only dropped a line that was //! ENTIRELY a comment. A needle planted in a trailing comment (`let _ = ctx; // the real call used @@ -85,14 +85,14 @@ pub(crate) fn shape_of(src: &str, signature: &str) -> String { } // ================================================================================================ -// Shared engine for a "raw table read outside its chokepoint" tripwire (issue #379). +// Shared engine for a "raw table read outside its chokepoint" tripwire. // ================================================================================================ // // `partition_discipline_tripwire::raw_scans` (spatial whole-table `.iter()`/`.count()`) and // `character_fence_tripwire::raw_lookups` (raw `game_character` point lookups) used to be ~70-line // near-clones of this — identical bound-handle walk-back, handle dedup and comment-line filtering // — and had already drifted from each other in small ways neither review caught. The exact drift -// pattern issue #64 consolidated once already, one layer up (`body_of`/`code_of`, above). One +// pattern consolidated once already, one layer up (`body_of`/`code_of`, above). One // engine now; the two callers differ only in which accessor(s) they watch and what counts as // "opens a read" they care about. @@ -237,7 +237,7 @@ pub(crate) fn raw_table_reads( // named file is not, that is a PATH TYPO, and the tripwire must fail on it. /// Concatenate every file in `module/src/debug/` (the `#[cfg(feature = "debug_reducers")]` -/// directory module #386 split out of the former single `debug.rs`) into one blob, so a text-scan +/// directory module split out of the former single `debug.rs`) into one blob, so a text-scan /// tripwire that used to `include_str!("debug.rs")` keeps seeing every debug reducer regardless of /// which of the seven files it landed in. Directory-read order (unspecified) is fine — every caller /// only substring/signature-searches, never anchors on cross-file position or file boundaries. @@ -350,7 +350,7 @@ pub(crate) fn read_scanned(rel: &str) -> Option { mod tests { use super::*; - /// The fixture the trailing-comment defeat looked like for real (round-1 review, #59): a `let + /// The fixture the trailing-comment defeat looked like for real (round-1 review): a `let /// _ = ctx;` no-op with the real call demoted to a trailing comment. The weak four-file version /// of `code_of` (`.filter(|l| !l.trim_start().starts_with("//"))`) left that comment's text /// intact, so `.contains("bump_guid_high_water")` still passed. This must now be gone. diff --git a/module/src/trade.rs b/module/src/trade.rs index 800f347d3..6459726c8 100644 --- a/module/src/trade.rs +++ b/module/src/trade.rs @@ -1,9 +1,9 @@ -//! Player-to-player trading (#8) — the Trade Session handshake (#120). +//! Player-to-player trading (#8) — the Trade Session handshake. //! //! A **Trade Session** (see `CONTEXT.md`) is transient module state, the `GroupInvite` template: //! private rows, defensive teardown, never Escrow — after a crash or teardown the trade simply -//! never happened. The **Trade Commit** (the atomic item/gold swap, #122) and the offer relay -//! (#121) build on the tables here; this file lands the handshake: initiate → `BeginTrade` to the +//! never happened. The **Trade Commit** (the atomic item/gold swap) and the offer relay +//! build on the tables here; this file lands the handshake: initiate → `BeginTrade` to the //! target → `CMSG_BEGIN_TRADE` → `OpenWindow` to both → cancel/logout → `TradeCanceled` to both. //! //! All trade statuses — success AND refusal, both parties — ride ONE path: recipient-keyed @@ -24,9 +24,9 @@ use crate::{ /// One Trade Session per player PAIR — and at most one per player, in either seat (the /// one-session-per-player invariant, enforced by [`initiate_verdict`]'s busy check). `open` is the /// handshake state: false = proposed (`BeginTrade` sent, window not yet open), true = both windows -/// open. The accept flags and per-seat gold are the Trade Commit's state (#122): both true runs +/// open. The accept flags and per-seat gold are the Trade Commit's state: both true runs /// [`run_trade_commit`]; any offer mutation clears both ([`reset_accepts`]). Private — clients see -/// trade state only through [`TradeEvent`]. Reaped on idle (#123, `gc.rs`); torn down on logout, +/// trade state only through [`TradeEvent`]. Reaped on idle (`gc.rs`); torn down on logout, /// death, and transfer. [entity] #[table(accessor = game_trade_session, index(accessor = by_initiator, btree(columns = [initiator_guid])), @@ -66,7 +66,7 @@ crate::character_owned!(not_transported, fn sweep_transfer_game_trade_session()) /// One offered item in a Trade Session — child rows of [`TradeSession`] (the one-row-plus-child /// idiom; a slot column per trade slot would be a fixed-width dead end). `trade_slot` 0..=5 are /// the traded slots; 6 is the Will-Not-Be-Traded Slot, shown but never committed. Unused until -/// #121 sets offers — landed with the session table so the schema chore runs once. Private. [entity] +/// Sets offers — landed with the session table so the schema chore runs once. Private. [entity] #[table(accessor = game_trade_slot, index(accessor = by_session, btree(columns = [session_id])))] pub struct TradeSlot { #[primary_key] @@ -123,7 +123,7 @@ fn push_trade_event(ctx: &ReducerContext, recipient_guid: u64, kind: u8, other_g push_trade_event_payload(ctx, recipient_guid, kind, other_guid, String::new()); } -/// [`push_trade_event`] with an `OFFER_*` snapshot payload (#121). +/// [`push_trade_event`] with an `OFFER_*` snapshot payload. fn push_trade_event_payload( ctx: &ReducerContext, recipient_guid: u64, @@ -154,7 +154,7 @@ pub(crate) fn session_involving(ctx: &ReducerContext, guid: u64) -> Option>() { @@ -164,7 +164,7 @@ fn remove_session(ctx: &ReducerContext, session: &TradeSession) { } /// A player-interaction-gate refusal as its wire kind — shared by the initiate verdict and the -/// commit-time re-validation (#122), so the two moments can never name the same failure +/// commit-time re-validation, so the two moments can never name the same failure /// differently. pub(crate) fn gate_refusal_kind(denied: PlayerInteractionDenied) -> u8 { match denied { @@ -193,7 +193,7 @@ pub(crate) fn initiate_verdict( if let Err(denied) = gate { return Err(gate_refusal_kind(denied)); } - // The AUTO-decline half of ignore (#123): the target never sees a proposal from someone they + // The AUTO-decline half of ignore: the target never sees a proposal from someone they // ignore — the server answers IgnoreYou at initiate time (the whisper-drop precedent). The // client-volunteered CMSG_IGNORE_TRADE relay is the other half. if target_ignores_initiator { @@ -270,7 +270,7 @@ pub(crate) fn apply_initiate_trade( pub(crate) const WILL_NOT_BE_TRADED_SLOT: u8 = 6; /// One offered item as the window renders it — the pure instance+template → wire-fields join -/// (the #121 "real stack count, durability, enchant" AC), factored out so the field mapping is +/// (the "real stack count, durability, enchant" AC), factored out so the field mapping is /// testable without a `ReducerContext`. `max_durability` comes from the TEMPLATE (instances only /// carry current durability). pub(crate) fn offer_slot_view( @@ -290,7 +290,7 @@ pub(crate) fn offer_slot_view( } /// Bump the session's `created_at` — every offer action refreshes it, which is what makes the -/// idle-TTL reap (#123) mean "2 minutes since the last ACTION", not "since the handshake". +/// idle-TTL reap mean "2 minutes since the last ACTION", not "since the handshake". /// Takes and returns the row (`update` consumes it) so callers keep a current copy without a /// `Clone` bound. fn touch_session(ctx: &ReducerContext, mut session: TradeSession) -> TradeSession { @@ -298,7 +298,7 @@ fn touch_session(ctx: &ReducerContext, mut session: TradeSession) -> TradeSessio ctx.db.game_trade_session().id().update(session) } -/// Relay `offerer_guid`'s whole current offer to both windows (#121): one snapshot payload, two +/// Relay `offerer_guid`'s whole current offer to both windows: one snapshot payload, two /// rows — `OFFER_SELF` back to the offerer, `OFFER_PARTNER` to the other seat. A slot row whose /// item no longer resolves (consumed/destroyed while offered) is deleted here rather than relayed /// — self-healing, the same posture as the movement relay's ghost rows. @@ -356,7 +356,7 @@ fn push_offer_events(ctx: &ReducerContext, session: &TradeSession, offerer_guid: ); } -/// The accept-reset rule's impure half (#122): if either party had accepted, clear BOTH flags +/// The accept-reset rule's impure half: if either party had accepted, clear BOTH flags /// and tell both windows `BackToTrade`. Returns the (possibly updated) session copy. fn reset_accepts(ctx: &ReducerContext, mut session: TradeSession) -> TradeSession { if !mutation_clears_accepts(session.initiator_accepted, session.target_accepted) { @@ -380,7 +380,7 @@ fn reset_accepts(ctx: &ReducerContext, mut session: TradeSession) -> TradeSessio session } -/// `CMSG_SET_TRADE_ITEM` core (#121): place the item in inventory slot `inv_slot` into window +/// `CMSG_SET_TRADE_ITEM` core: place the item in inventory slot `inv_slot` into window /// slot `trade_slot`. The gateway already mapped the client's (bag, slot) addressing onto the /// absolute slot (the item-family convention). No open session is a silent no-op (client races a /// cancel); a bad slot, an empty inventory slot, a soulbound item, or an item already offered in @@ -444,7 +444,7 @@ pub(crate) fn apply_set_trade_item( Ok(()) } -/// `CMSG_CLEAR_TRADE_ITEM` core (#121). Clearing an already-empty slot still echoes the snapshot +/// `CMSG_CLEAR_TRADE_ITEM` core. Clearing an already-empty slot still echoes the snapshot /// — cheap, and the client stays authoritative-synced. pub(crate) fn apply_clear_trade_item( ctx: &ReducerContext, @@ -476,8 +476,8 @@ pub(crate) fn apply_clear_trade_item( Ok(()) } -/// `CMSG_SET_TRADE_GOLD` core (#121): record the offered copper on the actor's seat. Balance is -/// NOT checked here — the Trade Commit (#122) re-validates everything at the only moment it +/// `CMSG_SET_TRADE_GOLD` core: record the offered copper on the actor's seat. Balance is +/// NOT checked here — the Trade Commit re-validates everything at the only moment it /// matters, and the client caps its own input meanwhile. pub(crate) fn apply_set_trade_gold( ctx: &ReducerContext, @@ -521,7 +521,7 @@ pub(crate) fn apply_begin_trade(ctx: &ReducerContext, actor: WorldEntity) -> Res return Ok(()); }; session.open = true; - // Opening counts as a trade action for the #123 idle reap. + // Opening counts as a trade action for the idle reap. session.created_at = ctx.timestamp; let (initiator, target) = (session.initiator_guid, session.target_guid); sessions.id().update(session); @@ -536,12 +536,12 @@ pub(crate) fn apply_cancel_trade(ctx: &ReducerContext, actor: WorldEntity) -> Re Ok(()) } -/// How long an idle Trade Session lives (#123). Its own named knob so trade idleness never +/// How long an idle Trade Session lives. Its own named knob so trade idleness never /// silently rides the group-invite dial — initialized FROM it because both are "a human is /// looking at a dialog" windows today. pub(crate) const TRADE_IDLE_TTL_MICROS: i64 = crate::INVITE_TTL_MICROS; -/// Is a session idle past [`TRADE_IDLE_TTL_MICROS`] (#123)? STRICTLY past — an exactly-at-TTL +/// Is a session idle past [`TRADE_IDLE_TTL_MICROS`]? STRICTLY past — an exactly-at-TTL /// session survives one more GC pass. `last_action_micros` is `created_at`, which /// [`touch_session`] bumps on every trade action, so this measures idleness since the last /// action, not total age. Pure; the sweep itself lives in `gc.rs` (the group-invite block shape). @@ -549,7 +549,7 @@ pub(crate) fn session_is_stale(last_action_micros: i64, now_micros: i64) -> bool now_micros - last_action_micros > TRADE_IDLE_TTL_MICROS } -/// `CMSG_BUSY_TRADE` core (#123): the proposed target's client declined as busy — kill the +/// `CMSG_BUSY_TRADE` core: the proposed target's client declined as busy — kill the /// proposal, tell the initiator `Busy`. Only a PROPOSED (not yet open) session declines this way; /// no proposal is a silent no-op (client races). The decliner's client showed nothing, so only /// the initiator hears a status. @@ -558,7 +558,7 @@ pub(crate) fn apply_busy_trade(ctx: &ReducerContext, actor: WorldEntity) -> Resu Ok(()) } -/// `CMSG_IGNORE_TRADE` core (#123): the proposed target has the initiator ignored — kill the +/// `CMSG_IGNORE_TRADE` core: the proposed target has the initiator ignored — kill the /// proposal, tell the initiator `IgnoreYou`. Same shape as [`apply_busy_trade`]. pub(crate) fn apply_ignore_trade(ctx: &ReducerContext, actor: WorldEntity) -> Result<(), String> { decline_proposal(ctx, actor.guid, event_kind::IGNORE_YOU); @@ -582,7 +582,7 @@ fn decline_proposal(ctx: &ReducerContext, target_guid: u64, kind: u8) { push_trade_event(ctx, session.initiator_guid, kind, session.target_guid); } -/// One seat's commit-relevant facts (#122) — gathered impurely by [`run_trade_commit`], judged +/// One seat's commit-relevant facts — gathered impurely by [`run_trade_commit`], judged /// purely by [`commit_verdict`] (the transfer-planner lesson: decisions pure, reducers thin). /// `items_offered` counts TRADED slots only — the Will-Not-Be-Traded Slot never reaches a commit. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -595,7 +595,7 @@ pub(crate) struct CommitSide { pub offers_soulbound: bool, } -/// Why a dual-accept commit refuses (#122) — `side` names the culprit seat so the wire answer +/// Why a dual-accept commit refuses — `side` names the culprit seat so the wire answer /// (whose bags, whose tampering) is deterministic. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum CommitRefusal { @@ -619,7 +619,7 @@ pub(crate) fn gold_after(balance: u32, outgoing: u32, incoming: u32) -> Option Result<(), CommitRefusal> { @@ -643,13 +643,13 @@ pub(crate) fn commit_verdict(a: &CommitSide, b: &CommitSide) -> Result<(), Commi Ok(()) } -/// The accept-reset rule's pure half (#122): does this offer mutation clear the accept flags? +/// The accept-reset rule's pure half: does this offer mutation clear the accept flags? /// Any change after EITHER accept voids both — the anti-scam floor. pub(crate) fn mutation_clears_accepts(initiator_accepted: bool, target_accepted: bool) -> bool { initiator_accepted || target_accepted } -/// `CMSG_ACCEPT_TRADE` core (#122): set the actor's accept flag; when BOTH seats have accepted, +/// `CMSG_ACCEPT_TRADE` core: set the actor's accept flag; when BOTH seats have accepted, /// run the Trade Commit in this same transaction. Until then the partner hears `TradeAccept`. pub(crate) fn apply_accept_trade(ctx: &ReducerContext, actor: WorldEntity) -> Result<(), String> { let Some(mut session) = session_involving(ctx, actor.guid).filter(|s| s.open) else { @@ -675,7 +675,7 @@ pub(crate) fn apply_accept_trade(ctx: &ReducerContext, actor: WorldEntity) -> Re Ok(()) } -/// `CMSG_UNACCEPT_TRADE` core (#122): withdraw the actor's accept; the partner hears +/// `CMSG_UNACCEPT_TRADE` core: withdraw the actor's accept; the partner hears /// `BackToTrade` (their own client already knows). A no-accept unaccept is a silent no-op. pub(crate) fn apply_unaccept_trade(ctx: &ReducerContext, actor: WorldEntity) -> Result<(), String> { let Some(mut session) = session_involving(ctx, actor.guid).filter(|s| s.open) else { @@ -742,7 +742,7 @@ fn gather_commit_side( ) } -/// The Trade Commit (#122): re-validate presence/range, judge the pure [`commit_verdict`], then +/// The Trade Commit: re-validate presence/range, judge the pure [`commit_verdict`], then /// swap items and gold atomically — the reducer transaction IS the atomicity; any `Err` rolls /// everything back and nothing moves. `acceptor` is the seat whose accept completed the pair /// (stale-window refusals answer them). @@ -912,7 +912,7 @@ pub(crate) fn cancel_trade_for(ctx: &ReducerContext, guid: u64) { mod tests { use super::*; - /// The swap arithmetic (#122): a purse never underflows (offering more than you hold) and + /// The swap arithmetic: a purse never underflows (offering more than you hold) and /// never wraps past the u32 cap — `None` from either side means the commit must refuse, so /// no coin is ever created or destroyed. Boundaries pinned exactly. #[test] @@ -937,7 +937,7 @@ mod tests { ); } - /// The commit gate (#122), enumerated per refusal: tampered gold, purse overflow, a raced + /// The commit gate, enumerated per refusal: tampered gold, purse overflow, a raced /// soulbound, and bag space NET of the slots a side's own offer frees. The a-side is checked /// first, pinned so the wire kinds name a deterministic culprit. #[test] @@ -993,7 +993,7 @@ mod tests { ); } - /// The accept-reset rule's pure half (#122): ANY offer mutation clears the flags if either + /// The accept-reset rule's pure half: ANY offer mutation clears the flags if either /// party had accepted — the anti-scam floor from the design session. #[test] fn any_offer_mutation_clears_accepts_when_either_party_accepted() { @@ -1022,7 +1022,7 @@ mod tests { ); } - /// The reap policy (#123): a session is stale only STRICTLY past the invite TTL since its + /// The reap policy: a session is stale only STRICTLY past the invite TTL since its /// last action — `touch_session` bumps `created_at` on every offer mutation and on window /// open, so this measures idleness, not total age. Boundary pinned: exactly-at-TTL survives. #[test] @@ -1033,7 +1033,7 @@ mod tests { assert!(!session_is_stale(1_000, 999), "clock skew never reaps"); } - /// The window-visible fields come from the right source (#121): live per-instance state + /// The window-visible fields come from the right source: live per-instance state /// (stack, current durability, enchant) off the INSTANCE, static identity (display id, max /// durability) off the TEMPLATE. Distinct values per field so a transposition can't pass. #[test] @@ -1119,7 +1119,7 @@ mod tests { Err(event_kind::TARGET_TO_FAR) ); - // Ignore auto-decline (#123): the target never sees the proposal. + // Ignore auto-decline: the target never sees the proposal. assert_eq!( initiate_verdict(ok, true, true, false, false), Err(event_kind::IGNORE_YOU) diff --git a/module/src/trainer.rs b/module/src/trainer.rs index eeaeef391..2c382f5f3 100644 --- a/module/src/trainer.rs +++ b/module/src/trainer.rs @@ -65,7 +65,7 @@ pub struct TrainerSpell { /// What a successful trainer purchase GRANTS (professions slice 3) — `crate::skill::learn_profession` for /// a flagged profession/weapon offering, `crate::spell::learn_spell_with_dependents` for a plain class /// spell. Each `apply_trainer_buy` arm constructs its OWN variant directly at the point it already knows -/// which one applies (issue #372) — there is no longer a separate `grant_for` routing function: the old +/// which one applies — there is no longer a separate `grant_for` routing function: the old /// shape threaded a `(to_learn, known, cap)` tuple with a `0u32` dead-sentinel `to_learn` on the two /// profession/weapon arms (never read on those arms — only `grant_for` re-derived, from `profession_line` /// ALONE, the exact branch each tuple was already built in) through to a `grant_for` call that just @@ -162,8 +162,8 @@ pub(crate) fn resolve_learn_target(ctx: &ReducerContext, spell_id: u32) -> u32 { /// Resolve + validate a trainer interaction: `trainer_guid` must be a real in-range TRAINER on `caster`'s /// own map+instance, and one that SERVES `caster`'s class — a Paladin trainer refuses a Warrior /// outright. Shared by [`apply_trainer_buy`] and `talent::do_reset_talents` (the respec path, -/// which is trainer-gated identically — its own comment used to read "Same gates as apply_trainer_buy") -/// (issue #372). Returns the resolved trainer entity. +/// which is trainer-gated identically — its own comment used to read "Same gates as apply_trainer_buy"). +/// Returns the resolved trainer entity. /// /// Bare error strings (no gtker `[N]` tag): `apply_trainer_buy` prefixes its own `"[0] "` tag on top so /// its wire-visible `SMSG_TRAINER_BUY_FAILED` text is unchanged; `do_reset_talents` is untagged (its @@ -477,7 +477,7 @@ mod tests { assert_eq!(trainer_buy_check(false, 40, 1, 1000, 100), Ok(())); } - // --- issue #22: the riding fork ------------------------------------------------------------------ + // --- the riding fork ------------------------------------------------------------------ /// A riding purchase is TIER-gated the same way a profession is: Apprentice-75 has not met the /// Journeyman-150 offering, so the second tier is buyable exactly once, and re-buying the tier you diff --git a/module/src/transfer/harness.rs b/module/src/transfer/harness.rs index 94524b28b..4f0299c35 100644 --- a/module/src/transfer/harness.rs +++ b/module/src/transfer/harness.rs @@ -4,7 +4,7 @@ //! //! `ReducerContext` cannot be constructed in a unit test, so every other test in `module/` is //! either a pure model or a source scan. That left `export_rows` / `import_rows` / `move_rows` / -//! `import_character_blob` pinned by their TEXT: the #36 review ran 21 mutations against that +//! `import_character_blob` pinned by their TEXT: the review ran 21 mutations against that //! surface and 17 left the suite green or hung it — including repointing //! `sweep_transfer_game_item_instance` at `not_transported` (which deletes every character's //! inventory and gear on every hop) with **468 passed, 0 failed**. @@ -24,7 +24,7 @@ //! test one. Every guard each reducer has is executed here against real bsatn blobs built by the //! real [`build_export_blob`] — including the whole six-step cross-database sequence, driven //! across TWO `FakeDb`s in -//! `the_six_step_sequence_moves_a_populated_character_between_two_databases` (#380). +//! `the_six_step_sequence_moves_a_populated_character_between_two_databases`. //! //! # The ceiling — what this harness still CANNOT run, and why //! @@ -40,7 +40,7 @@ //! - each REAL table's arm EXISTS (`every_manifest_table_can_cross_a_database_boundary`) — read //! off build.rs's generated `CHARACTER_OWNED_TRANSFER_NAMES`, not off source text; //! - each arm transports rather than declining -//! (`the_not_transported_allowlist_matches_the_arms_that_decline`) — likewise generated. #380 +//! (`the_not_transported_allowlist_matches_the_arms_that_decline`) — likewise generated. //! moved this from a 100-line brace-depth parser to the `character_owned!` marker KIND, so //! "does this arm actually move rows" is a parse-time property now, not a scan's guess; //! - the cross-database eviction keeps the instance LEASE @@ -52,10 +52,10 @@ //! total: no-op'ing `CtxShard::import_rows` means **no manifest table's rows ever arrive**, and an //! early `return Ok(())` in a reducer shim means the reducer the gateway calls does nothing at all //! while every test below still passes. It stays pinned by EXACT-SHAPE equality -//! (`tests::the_production_adapter_is_the_pass_through_the_harness_assumes`), and #380's +//! (`tests::the_production_adapter_is_the_pass_through_the_harness_assumes`), and the //! cargo-mutants run is what proved that pin still has to exist: a mutation tool can only ask //! whether a test FAILS, and 54 mutants across `CtxShard` were MISSED because no headless test can -//! execute a `ReducerContext` at all. What #380 DID retire is the pins over `begin_transfer`'s and +//! execute a `ReducerContext` at all. What DID retire is the pins over `begin_transfer`'s and //! `reap_transfers`' 120-line bodies — those bodies are `apply_begin`/`apply_reap` now, and this //! file runs them. //! * **What is still not covered anywhere headless**: SpacetimeDB's transaction rollback. A real @@ -101,7 +101,7 @@ pub struct RelayRow { /// One "database". `RefCell`, not `Mutex`, on purpose: a re-entrant access PANICS (a named /// test failure) instead of deadlocking. A hang is not a pass — see the same fix applied to the -/// gateway's `FakeShardDb` (issue #37). +/// gateway's `FakeShardDb`. #[derive(Default)] pub struct FakeDb { chars: RefCell>, @@ -114,13 +114,13 @@ pub struct FakeDb { #[allow(clippy::type_complexity)] /// `game_transfer_in`: transfer_id -> (character_guid, blob, created_micros). in_rows: RefCell, i64)>>, - /// `game_transfer_out`: transfer_id -> the escrow's routing fields (issue #34). + /// `game_transfer_out`: transfer_id -> the escrow's routing fields. out_rows: RefCell>, /// `game_group_member`: character_guid -> group_id. group_members: RefCell>, /// Parties torn down by `remove_member`'s disband — what the character-owned DELETE sweep /// does to a party when one of its members is deleted, and precisely what - /// `detach_for_transfer` exists to run AHEAD of (issue #19 AC#4). + /// `detach_for_transfer` exists to run AHEAD of (AC#4). disbanded: RefCell>, /// `game_character_shard`: the forwarding receipt. CHARACTER-OWNED, so the cascade sweeps /// it — which is why `apply_finish` must record it AFTER the cascade, never before. @@ -130,18 +130,18 @@ pub struct FakeDb { /// Makes `insert_character` silently do nothing — the ONE thing a real destination can do /// that this fake otherwise cannot: accept the call and materialise no row. That is the /// state `apply_import_blob`'s post-import PROOF exists to catch, and without a way to - /// reach it the proof was pinned only by its own text (a #36 mutation survivor). + /// reach it the proof was pinned only by its own text (a mutation survivor). swallow_inserts: Cell, - /// `game_guid_allocator.high_water` (issue #59): what `bump_guid_high_water` has ratcheted + /// `game_guid_allocator.high_water`: what `bump_guid_high_water` has ratcheted /// it to. Starts at 0 (unseeded), same as a fresh database. guid_high_water: Cell, - /// `game_guid_range` (issue #108/#237): the `(base, size)` THIS fake database mints from, + /// `game_guid_range`: the `(base, size)` THIS fake database mints from, /// if any. `None` (the default) models a database that never installed one — same as a /// fresh real database before `install_guid_range` runs. guid_range: Cell>, /// `game_transfer_reaper_schedule`: has `begin_transfer` armed the reaper on this database? /// A bool rather than a row, because the only thing the protocol asserts about it is that - /// the FIRST escrow arms it (#380) — a live database that never gets one has a frozen player + /// the FIRST escrow arms it — a live database that never gets one has a frozen player /// nobody ever recovers. reaper_armed: Cell, /// How many times `freeze_live_entity` ran. The live-entity delete is the fence covering @@ -152,7 +152,7 @@ pub struct FakeDb { } /// The escrow out-row's columns, kept as their own struct so the fake never needs `TransferOut` -/// itself to be `Clone`. It mirrors every column now (#380): `apply_begin` writes one and +/// itself to be `Clone`. It mirrors every column now: `apply_begin` writes one and /// `apply_confirm` copies the escrowed BLOB onto the attestation, so a fake that dropped the /// blob would have made the six-step sequence untestable end to end. #[derive(Clone, Default)] @@ -424,7 +424,7 @@ impl ShardLedger for FakeDb { } } -/// The #380 seam: `apply_begin` runs here for real — the freeze, the export, the blob build and +/// The seam: `apply_begin` runs here for real — the freeze, the export, the blob build and /// the escrow row, in order. impl BeginSink for FakeDb { fn has_active_taxi_flight(&self, guid: u64) -> bool { @@ -481,7 +481,7 @@ fn active_taxi_refuses_transfer_before_any_source_mutation() { assert!(!src.reaper_armed.get()); } -/// The issue #34 seam: `apply_finish` runs here for real, ordering included. +/// The seam: `apply_finish` runs here for real, ordering included. impl FinishSink for FakeDb { fn detach_for_transfer(&mut self, guid: u64) { // The RAW removal: membership goes, the party does not. @@ -551,7 +551,7 @@ impl ImportSink for FakeDb { const GUID: u64 = 4242; const NEIGHBOUR: u64 = 4343; -const XFER: u64 = GUID; // the transfer id IS the character guid (#19) +const XFER: u64 = GUID; // the transfer id IS the character guid // `o` is an arbitrary FACING (radians), not an approximation of π: the fixture only has to be a // legal orientation that survives the escrow round-trip byte-for-byte. #[allow(clippy::approx_constant)] @@ -563,14 +563,14 @@ const DEST: Destination = Destination { z: 12.5, o: 3.14, }; -/// The range a `dst` fixture installs in tests that need one (issue #237) — small and round +/// The range a `dst` fixture installs in tests that need one — small and round /// so a "foreign" guid reads as obviously outside it at a glance, deliberately not shaped /// like a real shard's billion-wide slot so a failing assertion's numbers read as test /// fixture, never as a live shard's actual range. const LOCAL_RANGE: (u64, u64) = (0, 1_000_000); /// A guid OUTSIDE `LOCAL_RANGE` — stands for a `lyracore-world-1`-born character (a /// different slot entirely) crossing into a database that owns `LOCAL_RANGE`. This is the -/// live #237 trigger: any Kalimdor-born character crossing into `lyracore`. +/// live trigger: any Kalimdor-born character crossing into `lyracore`. const FOREIGN_GUID: u64 = 5_000_000_000; /// A fully populated character — every field set to something DISTINGUISHABLE, so a field that @@ -830,7 +830,7 @@ fn export_import_export_produces_an_identical_blob() { assert_eq!(export(&third_db, GUID, XFER, here), first); // A hop to a DIFFERENT destination differs in the six positional fields and NOTHING else. - // Those six live on the CHARACTER ROW now (#380 deleted the blob's duplicate copies of + // Those six live on the CHARACTER ROW now (deleted the blob's duplicate copies of // them), so the assertion reads them off the arrived row rather than off the blob header. let moved = export(&dst, GUID, XFER, DEST); let mut far = FakeDb::new(); @@ -860,12 +860,12 @@ fn export_import_export_produces_an_identical_blob() { } // ======================================================================================= -// Issue #59 — importing a character ratchets the destination's guid allocator +// Importing a character ratchets the destination's guid allocator // ======================================================================================= /// AC#3: an imported character bumps the destination's guid high-water mark, so a /// `create_character` on THIS database afterward can never hand out the same guid. This is -/// the LOCAL-range half of issue #237's fix — `GUID` sits inside `dst`'s own `LOCAL_RANGE`, +/// the LOCAL-range half of the fix — `GUID` sits inside `dst`'s own `LOCAL_RANGE`, /// so the new gate must not regress this property: importing a character that belongs HERE /// still ratchets exactly as before. #[test] @@ -891,7 +891,7 @@ fn importing_a_character_bumps_the_destinations_guid_high_water() { /// The mark never moves backwards — importing a LOWER guid after a higher one is already /// tracked must not pull the mark down (it would re-open the higher guid to reuse). `dst` /// installs `LOCAL_RANGE` so `GUID`'s bump attempt actually reaches `bump_guid_high_water` -/// (and therefore its never-lowers rule) rather than being skipped by the #237 gate for an +/// (and therefore its never-lowers rule) rather than being skipped by the gate for an /// unrelated reason — the two properties are independent and this test must keep exercising /// the never-lowers one specifically. #[test] @@ -909,9 +909,9 @@ fn a_lower_imported_guid_does_not_pull_the_high_water_mark_down() { ); } -/// **Issue #237, the regression fixed here.** An arrival whose guid belongs to ANOTHER +/// **The regression fixed here.** An arrival whose guid belongs to ANOTHER /// shard's range must leave THIS database's allocator untouched — ranges are disjoint by -/// construction (#108), so `FOREIGN_GUID` can never collide +/// construction, so `FOREIGN_GUID` can never collide /// with anything `dst` mints, and ratcheting past it anyway is exactly what pushed /// `lyracore`'s real high-water mark to its own range end with zero local characters /// above it — every subsequent local `create_character` then failed @@ -920,7 +920,7 @@ fn a_lower_imported_guid_does_not_pull_the_high_water_mark_down() { #[test] fn importing_a_foreign_range_guid_leaves_the_destinations_allocator_untouched() { let src = FakeDb::populated(FOREIGN_GUID); - let blob = export(&src, FOREIGN_GUID, FOREIGN_GUID, DEST); // transfer_id IS the guid (#19) + let blob = export(&src, FOREIGN_GUID, FOREIGN_GUID, DEST); // transfer_id IS the guid let mut dst = FakeDb::new(); dst.guid_range.set(Some(LOCAL_RANGE)); // FOREIGN_GUID is nowhere near [0, 1_000_000) @@ -943,7 +943,7 @@ fn importing_a_foreign_range_guid_leaves_the_destinations_allocator_untouched() #[test] fn an_import_that_cannot_place_a_table_files_no_in_row() { - // The #36 mutation: `import_rows(..)?` -> `let _ = import_rows(..)`. A partial character is + // The mutation: `import_rows(..)?` -> `let _ = import_rows(..)`. A partial character is // committed and the in-row filed anyway, which licenses cascade-deleting the source copy // the missing rows came from. let src = FakeDb::populated(GUID); @@ -964,7 +964,7 @@ fn an_import_that_cannot_place_a_table_files_no_in_row() { ); } -/// Issue #42, the inverse of the test above and the one the drift contract forgot: a table the +/// The inverse of the test above and the one the drift contract forgot: a table the /// registry expects that the PAYLOAD does not carry. This used to import with a clean `Ok(())`, /// file the in-row, and license `finish_transfer` to destroy the complete source copy of a /// character that had arrived without that table. @@ -1037,7 +1037,7 @@ fn an_undecodable_table_payload_aborts_the_import() { #[test] fn a_manifest_from_another_build_is_refused() { - // #16's stated contract. Deleting this guard left 468 tests green. + // the stated contract. Deleting this guard left 468 tests green. let src = FakeDb::populated(GUID); let mut blob = export(&src, GUID, XFER, DEST); blob.manifest.push(ManifestEntry { @@ -1173,7 +1173,7 @@ fn a_stale_copy_from_an_earlier_hop_is_wiped_before_the_arrival_lands() { assert!(dst.has_in_row(XFER)); } -/// Issue #83: the wipe above only fired when `has_character(guid)` was true, so an ORPHANED +/// The wipe above only fired when `has_character(guid)` was true, so an ORPHANED /// owned row (this guid's OWN item/quest/etc. left behind with no accompanying `game_character` /// row and no `game_transfer_in` witness — the residual state a table-level witness cannot see, /// since it is a fact about a DIFFERENT table) survived untouched and a fresh import landed @@ -1224,7 +1224,7 @@ fn orphaned_owned_rows_with_no_character_row_are_wiped_before_a_fresh_import_lan #[test] fn money_folded_into_the_escrow_after_the_freeze_arrives_with_the_character() { - // #30's DEFER verdict, end to end: `credit_purse` on an in-transit character folds copper + // the DEFER verdict, end to end: `credit_purse` on an in-transit character folds copper // into the escrowed blob, and `apply_import_blob`'s `c.money = decoded.money` is the ONLY // thing that replays it at the destination. Deleting that line left 468 tests green. let src = FakeDb::populated(GUID); @@ -1264,8 +1264,8 @@ fn a_character_with_nothing_still_crosses() { /// The post-import durability PROOF, EXECUTED. `Apply` ⇒ the destination copy is durable is a /// claim the model makes; this is the only place it is checked against a destination that -/// accepted the insert and materialised nothing. Deleting the proof left 468 tests green in -/// #36, and it stayed scan-only through the first cut of this harness — the scan is defeated by +/// accepted the insert and materialised nothing. Deleting the proof left 468 tests green, +/// and it stayed scan-only through the first cut of this harness — the scan is defeated by /// leaving its own message in a dead branch, which is how it was found. #[test] fn an_arrival_that_never_materialised_files_no_in_row() { @@ -1295,7 +1295,7 @@ fn a_corrupt_blob_is_refused_rather_than_half_applied() { } /// The export loop's OWN guards, driven directly: the guid it hands each mover, and the -/// machinery filter. (`export_rows` passing guid `0` to every mover was a #36 survivor.) +/// machinery filter. (`export_rows` passing guid `0` to every mover was a survivor.) #[test] fn the_export_loop_hands_each_mover_the_transferring_guid() { let src = FakeDb::populated(GUID); @@ -1360,7 +1360,7 @@ fn the_import_loop_refuses_a_table_this_build_has_no_arm_for() { } // ----------------------------------------------------------------------------------------- -// `apply_finish` — the delete-last body, executed (issue #34). +// `apply_finish` — the delete-last body, executed. // // These four replace the `fn do_finish(` source scan that used to stand in for them. Each // states the live consequence of the single line it pins; deleting that line turns exactly @@ -1427,7 +1427,7 @@ fn a_same_database_finish_never_cascades_the_shared_character_row() { ); } -/// Issue #19 AC#4: a shard hop is not a departure. +/// AC#4: a shard hop is not a departure. #[test] fn finish_detaches_the_party_before_the_cascade_so_the_party_survives_the_hop() { let mut db = finishing(GUID, true); @@ -1457,7 +1457,7 @@ fn finish_detaches_the_party_before_the_cascade_so_the_party_survives_the_hop() ); } -/// Issue #34 / the #19 × #20 merge order, as a behaviour rather than a comment. +/// The merge order, as a behaviour rather than a comment. #[test] fn the_forwarding_receipt_survives_the_cascade_that_would_sweep_it() { let mut db = finishing(GUID, true); @@ -1494,7 +1494,7 @@ fn not_transported_exports_nothing_and_absorbs_anything() { } // ----------------------------------------------------------------------------------------- -// `apply_begin` — step 1, EXECUTED (#380). +// `apply_begin` — step 1, EXECUTED. // // Everything below used to be three `.contains()` scans of `begin_transfer`'s 120-line body // ("it deletes the live entity row", "it calls export_rows", "it calls build_export_blob"). @@ -1639,7 +1639,7 @@ fn begin_refuses_a_character_with_no_durable_row_and_a_reserved_id() { } // ----------------------------------------------------------------------------------------- -// `apply_confirm` / `apply_release` — the two cross-database-only steps, EXECUTED (#380). +// `apply_confirm` / `apply_release` — the two cross-database-only steps, EXECUTED. // ----------------------------------------------------------------------------------------- #[test] @@ -1710,7 +1710,7 @@ fn release_drops_the_arrival_fence_but_refuses_on_the_source() { } // ----------------------------------------------------------------------------------------- -// The WHOLE six-step cross-database sequence, across two FakeDbs (#380). +// The WHOLE six-step cross-database sequence, across two FakeDbs. // // Every step above is proven in isolation; this is the one that proves they COMPOSE — that // the blob step 1 wrote is the blob step 2 reads, that step 2b's attestation is what step 3 @@ -1797,7 +1797,7 @@ fn the_six_step_sequence_moves_a_populated_character_between_two_databases() { } // ----------------------------------------------------------------------------------------- -// `apply_reap` — the recovery net, EXECUTED (#380). +// `apply_reap` — the recovery net, EXECUTED. // // The `cross_database && !has_in` rule below used to be pinned by a `.contains()` scan of // `reap_transfers`' own text, which cannot tell the rule apart from its inverse. @@ -1905,7 +1905,7 @@ fn finish_before_the_attestation_refuses_and_leaves_the_source_copy_alone() { ); // ...and on a database with no escrow at all it is a logged NO-OP, not an error: the - // gateway fans a finish out across shards and only one of them holds the claim (#99). + // gateway fans a finish out across shards and only one of them holds the claim. let mut elsewhere = FakeDb::new(); apply_finish_step(&mut elsewhere, XFER).expect("no escrow here — nothing to finish"); } diff --git a/module/src/transfer/mod.rs b/module/src/transfer/mod.rs index 037c4cbdb..80d8ecb15 100644 --- a/module/src/transfer/mod.rs +++ b/module/src/transfer/mod.rs @@ -1,7 +1,7 @@ -//! Escrowed character transfer — the ONE primitive behind every sharding granularity (issue #16, -//! spec issue #12: instance entry, continent travel, seam crossings, bulk region migration). +//! Escrowed character transfer — the ONE primitive behind every sharding granularity ( +//! spec instance entry, continent travel, seam crossings, bulk region migration). //! -//! # Where the pieces live (#380) +//! # Where the pieces live //! //! | file | what is in it | //! |---|---| @@ -38,7 +38,7 @@ //! //! While either escrow row exists the character is *in transit* — [`is_in_transit`]. Four chokepoints //! enforce that, and it is worth being precise about which side each one covers, because the epic -//! (#19, the real cross-database move) is built on top of this claim: +//! (the real cross-database move) is built on top of this claim: //! //! 1. `helpers::entity_by_owner` — the ACTOR side. Every player-fired reducer resolves "who is //! acting" through it, so an in-transit character can take no action itself. @@ -47,13 +47,13 @@ //! 3. `begin_transfer`'s delete of the live `game_world_entity` row — the TARGET side. The ~50 //! hand-rolled `map_id`/`instance_id` gates, the aggro candidate scan, the threat lists and the //! AOI relay all resolve through that row, so they stop seeing the character by construction. -//! 4. `helpers::character_by_guid` / `character_by_name` — the BY-GUID side (issue #30). Reducers +//! 4. `helpers::character_by_guid` / `character_by_name` — the BY-GUID side. Reducers //! that reach a character by guid or by name straight into `game_character` (or into a //! character-owned table) touch none of the first three; this pair is the gate they route //! through, and it reads an in-transit character as ABSENT so each caller's existing //! "no such character" arm fires (no new error string, no gateway edit). //! -//! # The by-guid verdict table (issue #30) +//! # The by-guid verdict table //! //! REFUSE is not the right answer everywhere, so the class is settled per path. Four verdicts exist: //! @@ -67,7 +67,7 @@ //! `Character.owner_identity` is per-CONNECTION derived state; a carried copy would arrive stale //! and be overwritten immediately, and a field that is always wrong on arrival is worse than no //! field. Pinned by `owner_identity_is_regenerated_at_the_destination_never_carried`. -//! * **NOT A TRANSFER CONCERN** — the group verbs, settled by #22: party membership is +//! * **NOT A TRANSFER CONCERN** — the group verbs, settled: party membership is //! authoritative on realm-core, so there is no source-copy write left to lose. What replaced the //! fence question is a REPLICATION one, answered by putting `game_group_member` on //! [`NOT_TRANSPORTED`] and having the gateway re-push realm-core's roster at world entry. @@ -75,7 +75,7 @@ //! The audited exception list is EMPTY: every by-guid path in the tree is either fenced or holds one //! of the three other verdicts. **The issue-by-issue reasoning for each one — which path, which //! field it would have lost, why its verdict is what it is — is archived at -//! `docs/history/transfer-by-guid-verdict-table.md` (#380).** Read it before deciding a NEW path's +//! `docs/history/transfer-by-guid-verdict-table.md`.** Read it before deciding a NEW path's //! verdict; nothing in it is stale, it is simply not something the protocol's own source has to //! restate. //! @@ -84,10 +84,10 @@ //! them), and `tripwires.rs`'s `character_fence_tripwire` is the ratchet that stops a NEW unfenced //! by-guid path from being added. //! -//! # The CROSS-DATABASE protocol (issue #19) +//! # The CROSS-DATABASE protocol //! -//! #16 shipped the state machine within ONE database (two `instance_id` partitions, so -//! `import_character` re-partitions a row rather than materialising a second). #19 makes the move +//! Shipped the state machine within ONE database (two `instance_id` partitions, so +//! `import_character` re-partitions a row rather than materialising a second). The cross-database work makes the move //! real: two SpacetimeDB databases, with the gateway carrying the blob between them. The escrow is //! not forked — the same ledger, the same `plan_*` verdicts, the same reaper — but two facts the //! same-database deployment gets for free have to be supplied explicitly: @@ -128,8 +128,8 @@ //! - `do_finish` cascade-deletes the source copy for a cross-database escrow (there IS a second copy //! now), which is a no-op same-database by construction. //! -//! What is still parked: the LIVE two-database run. Every acceptance criterion of #19 needs a real -//! 1.12.1 client, two published databases and an operator — see issue #231. What is proven +//! What is still parked: the LIVE two-database run. Every acceptance criterion needs a real +//! 1.12.1 client, two published databases and an operator — What is proven //! headlessly is the state machine (the crash matrix in `tests.rs`, now walked for the six-step //! cross-database sequence too), the transport ratchet, and the whole six-step sequence executed //! across two `FakeDb`s in `harness.rs`. @@ -137,7 +137,7 @@ //! # Why every step is written against a SINK //! //! `ReducerContext` cannot be constructed in a unit test, so before the sinks existed the only thing -//! a test in this crate could do to a reducer body was read its TEXT. The #36 review ran 21 +//! a test in this crate could do to a reducer body was read its TEXT. The review ran 21 //! mutations against that surface and 17 left the suite green. Each step therefore has its body //! written against a trait — [`BeginSink`], [`ImportSink`], [`FinishSink`], [`ReapSink`], all over //! the shared [`ShardLedger`] — with [`CtxShard`] as the one production adapter and @@ -187,7 +187,7 @@ const TRANSFER_REAP_INTERVAL_MICROS: u64 = 5_000_000; // 5s /// Note: the spec asks for the in-transit flag as a COLUMN on `game_character`. It lives here /// instead because `game_character` is gateway-subscribed with a HAND-MAINTAINED binding /// (`docs/danger-zones.md` §1.2 / §1.6) — a column there needs a lockstep gateway edit, which is -/// issue #19's tree, not this one's. The ledger row is a strictly stronger flag anyway (it carries +/// the tree, not this one's. The ledger row is a strictly stronger flag anyway (it carries /// the destination and the blob), and the `by_character` index makes the check one indexed lookup. /// Upgrade path: fold into a `Character.in_transit_id` column when the gateway bindings are /// regenerated for multi-shard routing. @@ -213,7 +213,7 @@ pub struct TransferOut { /// bsatn of [`ExportBlob`]. pub blob: Vec, pub created_micros: i64, - /// **Is the destination a different DATABASE?** (issue #19.) Two things hang off it, and both + /// **Is the destination a different DATABASE?** Two things hang off it, and both /// are dupe/loss safety, not convenience: /// /// 1. `do_finish` must `cascade_delete_character` the source copy — same-database there is no @@ -224,7 +224,7 @@ pub struct TransferOut { /// "not imported" for a transfer that imported perfectly — and `recovery` would roll BACK /// past the point of no return, duplicating the character. See [`reap_transfers`]. /// - /// END-appended + `#[default(false)]` → additive auto-migration; every pre-#19 escrow row reads + /// END-appended + `#[default(false)]` → additive auto-migration; every earlier escrow row reads /// as same-database, which is exactly what it was. #[default(false)] pub cross_database: bool, @@ -440,16 +440,16 @@ pub(crate) fn plan_begin( /// Which character the ledger rows filed under one transfer id name: the SOURCE out-row's, with /// the DESTINATION in-row's as the fallback. /// -/// The fallback is not tidiness. Now that the transfer id IS the character guid (#19), a database +/// The fallback is not tidiness. Now that the transfer id IS the character guid, a database /// holding only an unreleased ARRIVAL in-row — the state left by a driver killed between /// `finish_transfer` and `release_transfer` — would otherwise read as an UNUSED id. `plan_begin` /// would then answer `Escrow`... except it never gets there, because [`is_in_transit`] sees the /// in-row and answers `AlreadyInTransit`, so the character is refused a transfer OUT of the shard -/// it is stuck on, permanently, with no operator recourse (the #36 review's blocker 2). Reading the +/// it is stuck on, permanently, with no operator recourse (the review's blocker 2). Reading the /// in-row here is what turns that into the `Replay` the `settle_transfer` fence-clear then repairs. /// /// Pure, so it is pinned by a real assertion rather than by the reducer's text — the whole line was -/// a #36 mutation survivor (issue #37). +/// a mutation survivor. pub(crate) fn escrowed_guid(out_row: Option, in_row: Option) -> Option { out_row.or(in_row) } @@ -515,7 +515,7 @@ pub(crate) enum Recovery { /// guessing roll-forward against a failed one destroys it. Same-database (this ticket) always /// answers `Some(..)` by reading the in-row directly. /// -/// Note: cross-database (issue #19) the answer comes from the gateway querying the destination +/// Note: cross-database, the answer comes from the gateway querying the destination /// shard, and `None` — destination unreachable — holds the escrow indefinitely. That is the correct /// failure mode: a frozen character is recoverable, a duplicated or deleted one is not. Escalation /// (alerting an operator on a long-held escrow) is ops tooling, not this primitive. @@ -566,7 +566,7 @@ pub(crate) fn is_in_transit(ctx: &ReducerContext, character_guid: u64) -> bool { /// Every instance id an in-transit character has a claim on: the escrow's DESTINATION and the /// source instance `begin_transfer` parked on the durable row. Consumed by -/// `instance::occupied_instances` (issue #30, REFUSE verdict) so the instance reaper cannot tear +/// `instance::occupied_instances` (REFUSE verdict) so the instance reaper cannot tear /// down an instance — and with it the character's `game_instance_binding` manifest rows — while a /// transfer into or out of it is still in flight. /// @@ -592,7 +592,7 @@ pub(crate) fn in_transit_instances(ctx: &ReducerContext) -> Vec { out } -/// DEFER verdict (issue #30) — fold a post-`begin_transfer` `money` credit into the escrowed export +/// DEFER verdict — fold a post-`begin_transfer` `money` credit into the escrowed export /// blob, so value that lands after the character was serialized still travels with it. Returns /// `true` when the delta was deferred (the caller is talking to an in-transit character), `false` /// when there is nothing in escrow and the caller should just write the row. @@ -606,9 +606,10 @@ pub(crate) fn in_transit_instances(ctx: &ReducerContext) -> Vec { /// nothing reads the blob again — same-database that is harmless because both partitions share the /// one `game_character` row the caller also writes directly, so no copper is lost either way. /// Note: the residual post-import window is the ONE place a cross-database delta would still -/// need a destination-side replay hop, and that hop is #19's (it owns the gateway leg that would -/// carry it). Folding unconditionally here means the final blob is already correct when #19 wires -/// `finish_transfer` to ship residual deltas — no call-site edit at that point. +/// need a destination-side replay hop, and that hop belongs to the cross-database work (it owns +/// the gateway leg that would carry it). Folding unconditionally here means the final blob is +/// already correct when that work wires `finish_transfer` to ship residual deltas — no call-site +/// edit at that point. pub(crate) fn defer_money_delta(ctx: &ReducerContext, character_guid: u64, amount: u32) -> bool { let outs = ctx.db.game_transfer_out(); let Some(mut row) = outs.by_character().filter(character_guid).next() else { @@ -712,7 +713,7 @@ pub(crate) trait BeginSink: ShardLedger { /// What **step 2, cross-database** touches, on the DESTINATION database. /// -/// This trait IS the seam of issue #37. `ReducerContext` cannot be constructed in a unit test, so +/// This trait IS the seam. `ReducerContext` cannot be constructed in a unit test, so /// before it existed nothing in the crate could execute the import — its guards were pinned by /// source scans that matched their own text, and 17 of 21 mutations against them left the suite /// green. @@ -724,15 +725,15 @@ pub(crate) trait ImportSink: ShardLedger { /// The payload half — [`import_rows`] against this database's transport registry. fn import_rows(&mut self, guid: u64, payload: &[TableRows]) -> Result<(), String>; fn ensure_shadow_account(&mut self, account_id: u64); - /// Ratchet THIS database's guid high-water mark up to at least `guid` (issue #59 AC#3) — so a + /// Ratchet THIS database's guid high-water mark up to at least `guid` (AC#3) — so a /// locally created character can never later collide with one this database received by import. /// Unconditional, same as `world::cascade_delete_character`'s call through this same method — /// the GATE on whether an arriving guid is even allowed to reach here lives in - /// [`apply_import_blob`] (issue #237), not in this method, precisely so the two sinks can never + /// [`apply_import_blob`], not in this method, precisely so the two sinks can never /// diverge on it (see `own_guid_range`'s doc comment). fn bump_guid_high_water(&mut self, guid: u64); /// THIS database's own `game_guid_range` (`(base, size)`), if one is installed. A FACT query, - /// not a decision — issue #237's [`apply_import_blob`] is what turns it into "should this + /// not a decision — the [`apply_import_blob`] is what turns it into "should this /// arrival ratchet the allocator" via `auth::in_guid_range`, so both sinks share the one /// decision instead of each re-implementing it. Putting the gate here instead would let a /// `FakeDb` that forgot to reimplement it silently validate a shard-poisoning import as green. @@ -741,7 +742,7 @@ pub(crate) trait ImportSink: ShardLedger { /// What **step 3** touches, on the SOURCE database. /// -/// The same seam as [`ImportSink`], for the same reason (issue #37, extended by #34). +/// The same seam as [`ImportSink`], for the same reason (extended). /// [`apply_finish`]'s ORDER — detach, then cascade, then write the forwarding receipt, then delete /// the escrow — was pinned only by source scans that matched their own text, and the ordering /// constraint is not cosmetic: `game_character_shard` is itself character-owned, so a cascade that @@ -763,7 +764,7 @@ pub(crate) trait ReapSink: FinishSink { } /// The ONE production adapter: the real `ReducerContext`, wearing every sink at once. It used to be -/// three separate structs (#380 merged them, so there is one place a `ctx.db` line can be wrong +/// three separate structs (merged them, so there is one place a `ctx.db` line can be wrong /// instead of three). /// /// **This layer is the seam's own blind spot and there is no headless way to close it.** The harness @@ -773,7 +774,7 @@ pub(crate) trait ReapSink: FinishSink { /// struct on the first full run, which is why `.cargo/mutants.toml` excludes it BY NAME with that /// number written down). So it stays pinned by exact-shape equality, in /// `tests::the_production_adapter_is_the_pass_through_the_harness_assumes` — the one string pin -/// #380 kept. Every method below must remain a single expression; if this stops being a layer of +/// Kept. Every method below must remain a single expression; if this stops being a layer of /// pass-throughs, the harness underneath it stops meaning what it claims. struct CtxShard<'a> { ctx: &'a ReducerContext, @@ -985,7 +986,7 @@ pub fn begin_transfer( } /// The whole of [`begin_transfer`] bar the operator gate, over a [`BeginSink`]. Executed for real by -/// `harness` (#380) — before that it was 120 lines of `ReducerContext` code whose only coverage was +/// `harness` — before that it was 120 lines of `ReducerContext` code whose only coverage was /// three `.contains()` scans of its own text. pub(crate) fn apply_begin( sink: &mut S, @@ -1031,7 +1032,7 @@ pub(crate) fn apply_begin( // because those all resolve through `game_world_entity`. sink.freeze_live_entity(character_guid); - // The ROWS (issue #19). Serialized here, in the same transaction that froze the character, so + // The ROWS. Serialized here, in the same transaction that froze the character, so // the payload is a consistent snapshot: nothing can write a manifest table between the freeze // and the export (the in-transit fence covers every writer bar the audited exceptions in this // module's verdict table). The character row is read AFTER the freeze — `freeze_live_entity` @@ -1079,8 +1080,8 @@ pub(crate) fn apply_begin( /// **Step 2 — commit the arrival copy. Idempotent on `transfer_id` (unique PK).** /// -/// Same-database tracer: the escrowed blob is read from the local out-row. Cross-database (issue -/// #19) the gateway ships the blob and [`import_character_blob`] takes it as an argument — the ONLY +/// Same-database tracer: the escrowed blob is read from the local out-row. Cross-database, +/// the gateway ships the blob and [`import_character_blob`] takes it as an argument — the ONLY /// difference, which is why the state machine is provable now. /// /// The ONE reducer in this module still written straight against `ReducerContext` rather than a @@ -1172,7 +1173,7 @@ pub fn import_character_blob( } /// The whole of [`import_character_blob`] bar the operator gate — every guard, in order, over an -/// [`ImportSink`]. Executed for real by `harness` (issue #37). +/// [`ImportSink`]. Executed for real by `harness`. pub(crate) fn apply_import_blob( sink: &mut S, transfer_id: u64, @@ -1219,7 +1220,7 @@ pub(crate) fn apply_import_blob( // Materialise `game_character`. Decoded with THIS build's `Character` type, then relocated to // the destination coordinates and given the money the blob carries (which is the escrowed value - // PLUS any `defer_money_delta` folded in after the freeze — issue #30's residual, replayed here + // PLUS any `defer_money_delta` folded in after the freeze — the residual, replayed here // because this is the hop that reads the blob at the destination). let mut c: crate::character::Character = spacetimedb::sats::bsatn::from_slice(&decoded.character_row).map_err(|e| { @@ -1238,7 +1239,7 @@ pub(crate) fn apply_import_blob( // wiped before the re-insert, so the arriving payload never lands on top of rows that would // PK-collide (or worse, survive as a second, older loadout). // - // UNCONDITIONAL (issue #83): this used to run only `if sink.has_character(guid)`, which wipes + // UNCONDITIONAL: this used to run only `if sink.has_character(guid)`, which wipes // every owned table when `game_character` itself survived — but says nothing about a table // that has this guid's rows WITHOUT a `game_character` row to key off. That combination is // exactly the one no transfer-id witness can see (the witness is a fact about @@ -1251,28 +1252,28 @@ pub(crate) fn apply_import_blob( // replay. See `orphaned_owned_rows_with_no_character_row_are_wiped_before_a_fresh_import_lands`. sink.cascade_delete_character(guid); sink.insert_character(c); - // Issue #59 AC#3: ratchet this database's guid allocator past `guid` NOW, in the same + // AC#3: ratchet this database's guid allocator past `guid` NOW, in the same // transaction as the materialisation — so a `create_character` racing this import (or run any // time after) can never hand the same guid to a brand-new local character. // - // ONLY when `guid` is actually inside THIS database's own range (issue #237). Ranges are - // disjoint by construction (#108) — a foreign-range arrival (e.g. a world-1 character crossing + // ONLY when `guid` is actually inside THIS database's own range. Ranges are + // disjoint by construction — a foreign-range arrival (e.g. a world-1 character crossing // into core) can never collide with anything core mints, so ratcheting past it protects // nothing and instead walks core's own mark toward, and eventually past, its own range end. // That is exactly what was hit live: `lyracore`'s high_water sat at its range end with // zero local characters above it, and every local `create_character` failed - // GUID_RANGE_EXHAUSTED. A local-range arrival still ratchets, unchanged from #59 AC#3. + // GUID_RANGE_EXHAUSTED. A local-range arrival still ratchets, unchanged AC#3. if crate::auth::in_guid_range(sink.own_guid_range(), guid) { sink.bump_guid_high_water(guid); } sink.import_rows(guid, &decoded.payload)?; // The destination has no `game_account` row (accounts are realm-scoped and live on the default - // database until realm-core, #22). `gw::gw_player_login` resolves the account by id, so + // database until realm-core). `gw::gw_player_login` resolves the account by id, so // without one the arriving player cannot log in at all. // Deliberate simplification: a SHADOW account — id + a synthetic username, no credentials. The // gateway rebinds `identity` through `establish_session` on this shard at every world entry, which is the only - // field this row exists to hold. Upgrade path: realm-core owns accounts (#12 Phase B) and this + // field this row exists to hold. Upgrade path: realm-core owns accounts (Phase B) and this // whole arm goes away. Never a login credential: SRP runs on the logon tier against the realm // database, never here. sink.ensure_shadow_account(account_id); @@ -1315,7 +1316,7 @@ pub(crate) fn apply_import_blob( /// /// So the gateway, which is the one component that can see both databases, writes the in-row HERE /// after `import_character_blob` has COMMITTED at the destination. Everything downstream — -/// `plan_finish`, `recovery`, the whole #16 crash matrix — then works unchanged, because the in-row +/// `plan_finish`, `recovery`, the whole crash matrix — then works unchanged, because the in-row /// still means precisely what it meant: *the destination copy is durable*. /// /// **The obligation this moves out of the module**: nothing here can check that the import really @@ -1417,7 +1418,7 @@ pub(crate) fn apply_finish_step( let has_out = sink.out_row(transfer_id).is_some(); let has_in = sink.in_row(transfer_id).is_some(); match plan_finish(has_out, has_in) { - // NO-OP, and it must SAY so (#99). The gateway's drive calls `finish_transfer` on a specific + // NO-OP, and it must SAY so. The gateway's drive calls `finish_transfer` on a specific // database and reads `Ok(())` as "the source released the character", then proceeds to // publish_shard_index / release_transfer / evict. Returning success silently here makes a // finish driven against the WRONG database indistinguishable from one that did the work — @@ -1441,11 +1442,11 @@ pub(crate) fn apply_finish_step( } /// The delete-last body, shared by [`finish_transfer`] and the reaper's roll-forward. Executed for -/// real — order and all — by `harness` (issue #34). +/// real — order and all — by `harness`. pub(crate) fn apply_finish(sink: &mut S, transfer_id: u64) { let mut cascaded = false; if let Some(out) = sink.out_row(transfer_id) { - // CROSS-DATABASE (issue #19): the source shard's teardown is `cascade_delete_character` — + // CROSS-DATABASE: the source shard's teardown is `cascade_delete_character` — // the existing character-owned delete sweep, the exact counterpart of the payload this blob // exported. Skipping it leaves a full second copy of the character on the source, which is a // dupe the moment anything logs it in. Same-database there is nothing to cascade: the two @@ -1454,7 +1455,7 @@ pub(crate) fn apply_finish(sink: &mut S, transfer_id: u64) { // row — the source's claim on the character. cascaded = out.cross_database; if out.cross_database { - // The group MIRROR (issue #19, AC#4) — done HERE, before the cascade, rather than inside + // The group MIRROR (AC#4) — done HERE, before the cascade, rather than inside // `sweep_delete_game_group_member`, so it depends on no sweep ordering. A shard hop is not // a departure: `remove_member`'s leader-transfer/disband would tear the party down on the // source the instant the FIRST member hops, and the second member would then arrive with no @@ -1465,11 +1466,11 @@ pub(crate) fn apply_finish(sink: &mut S, transfer_id: u64) { sink.detach_for_transfer(out.character_guid); sink.cascade_delete_character(out.character_guid); } - // #20 AC#3: the character→shard index entry is written HERE, inside the same transaction that + // AC#3: the character→shard index entry is written HERE, inside the same transaction that // releases the escrow, from the out-row's own destination fields — so "the escrow settled" and // "the directory says where it settled" can never disagree on this database. // - // Cross-database, this is only HALF the index (issue #34). There is no transaction spanning + // Cross-database, this is only HALF the index. There is no transaction spanning // two SpacetimeDB databases, so realm-core's copy — the one `home_shard` actually reads — // cannot be written from here at all. What the gateway does instead is REPLICATE this row: // `world::transfer::run_transfer` publishes the same `(guid, map, instance)` to realm-core @@ -1486,7 +1487,7 @@ pub(crate) fn apply_finish(sink: &mut S, transfer_id: u64) { // have to be fixed together before anything routes on it. See // `gateway/src/realm_core.rs::publish_shard_index`. // - // MERGE ORDER (#19 × #20): this runs AFTER the cascade, never before. `game_character_shard` + // MERGE ORDER: this runs AFTER the cascade, never before. `game_character_shard` // is itself character-owned (`realm_core.rs`'s delete sweep), so a cascade that ran second // would wipe the forwarding receipt the source shard is supposed to keep — the receipt that // lets a gateway whose realm-core is unconfigured (`realm_core()` == the default shard) still @@ -1494,7 +1495,7 @@ pub(crate) fn apply_finish(sink: &mut S, transfer_id: u64) { sink.record_shard(out.character_guid, out.dest_map_id, out.dest_instance_id); } sink.clear_escrow(transfer_id); - // Name what actually happened (#99): "complete" alone read identically whether the source copy + // Name what actually happened: "complete" alone read identically whether the source copy // was cascade-deleted or the transfer was same-database (where there is no source copy to // destroy). "finish ran" and "the source copy is gone" are different facts and were one line. log::info!( @@ -1518,14 +1519,14 @@ pub fn reap_transfers(ctx: &ReducerContext, _schedule: TransferReaperSchedule) { } /// The whole of [`reap_transfers`] bar the scheduler-identity gate, over a [`ReapSink`]. Executed -/// for real by `harness` (#380) — the `cross_database && !has_in` rule below used to be pinned by a +/// for real by `harness` — the `cross_database && !has_in` rule below used to be pinned by a /// `.contains()` scan of the reducer's own text. pub(crate) fn apply_reap(sink: &mut S) { let now = sink.now_micros(); for (transfer_id, character_guid, created_micros, cross_database) in sink.escrows() { let age = now - created_micros; let has_in = sink.in_row(transfer_id).is_some(); - // CROSS-DATABASE (issue #19): the destination's arrival copy is on ANOTHER database, so an + // CROSS-DATABASE: the destination's arrival copy is on ANOTHER database, so an // ABSENT in-row here does not mean "not imported" — it means "not yet ATTESTED" // ([`confirm_import`]). Reading it as `Some(false)` would roll the escrow BACK past the // point of no return, unfreezing the source copy while the destination copy is live: a diff --git a/module/src/transfer/tests.rs b/module/src/transfer/tests.rs index 6d08500a6..06c091585 100644 --- a/module/src/transfer/tests.rs +++ b/module/src/transfer/tests.rs @@ -1,7 +1,7 @@ //! The crash matrix, the pure-planner enumerations, and the source-scan tripwires that pin the //! fences a pure model cannot see. //! -//! Split out of `transfer.rs` by #380; `harness.rs` holds the half that EXECUTES the protocol +//! Split out of `transfer.rs`; `harness.rs` holds the half that EXECUTES the protocol //! against two in-memory databases. use super::*; @@ -41,10 +41,10 @@ fn manifest_is_the_generated_enumeration_minus_the_machinery() { } /// [`MANIFEST_EXCLUDE`] is the ONLY input to three separate subtractions — the manifest, the -/// export loop, and (issue #42) the arriving payload's required set — and nothing pinned its +/// export loop, and the arriving payload's required set — and nothing pinned its /// CONTENTS. Verified by mutation: adding a real character-owned table to it left all 501 /// module tests green while that table silently vanished from the manifest, was never exported, -/// and was no longer required of an arriving payload. That is the #42 defect reintroduced one +/// and was no longer required of an arriving payload. That is the defect reintroduced one /// name at a time, and it is data loss (a transfer would drop the rows) rather than a missing /// popup. The test above cannot see it: both sides of its length equation move together. /// @@ -93,7 +93,7 @@ fn hot_marks_name_only_real_manifest_tables() { } // ------------------------------------------------------------------------------------- -// Issue #72 hot-state audit: auras ON the character (buffs/debuffs/Stealth) must ride the +// Hot-state audit: auras ON the character (buffs/debuffs/Stealth) must ride the // blob like every other manifest table. Before this, `game_aura` had NO `character_owned!` // marker at all (its columns name `target_guid`/`caster_guid`, neither of which the tripwire // in tripwires.rs recognizes), so a warm handoff silently dropped every buff, DoT, HoT and the @@ -119,7 +119,7 @@ fn aura_rows_are_a_manifest_table_marked_hot() { ); } -/// The exact question issue #72 asked: does a 10-minute buff resume with the right REMAINING +/// The exact question asked: does a 10-minute buff resume with the right REMAINING /// time, or does the export/import round trip re-base it? `applied_at`/`expires_at` are /// `Timestamp` — an ABSOLUTE point in wall-clock time, never a duration — so the answer should /// be "unchanged bit-for-bit", and remaining-duration-against-a-fixed-`now` should therefore be @@ -215,7 +215,7 @@ fn aura_absolute_expiry_survives_the_export_import_round_trip_with_the_same_rema /// with an unrelated row the destination already minted under that same id, or (same-value /// coincidence aside) simply means nothing there. /// -/// Since #380 that is one word in the arm's declaration (`remint = id` vs `keep_key`) rather +/// Since that is one word in the arm's declaration (`remint = id` vs `keep_key`) rather /// than a hand-written `row.id = 0` the macro cannot see — so this scan asserts the DECLARED /// choice. `keep_key` is right for exactly one table in the tree (`game_item_instance`, whose /// guid is derived from the owner and is the id the CLIENT knows the item by); everything else @@ -707,7 +707,7 @@ fn reaper_is_inert_before_the_stale_window_and_with_no_escrow() { ); } -/// #223 — the ENTIRE recovery decision, enumerated rather than sampled. +/// The ENTIRE recovery decision, enumerated rather than sampled. /// /// The tests above sample `recovery` at the points that mattered when each was written. This /// walks every combination of its three inputs — `has_out` × `dest_imported` (all three states, @@ -826,7 +826,7 @@ fn the_pure_planners_are_enumerated_over_their_whole_input_space() { ); assert!(!login_allowed(true, true)); - // escrowed_guid — out-row first, in-row as the fallback that un-strands #36's blocker 2. + // escrowed_guid — out-row first, in-row as the fallback that un-strands blocker 2. assert_eq!(escrowed_guid(None, None), None); assert_eq!(escrowed_guid(Some(7), None), Some(7)); assert_eq!( @@ -924,14 +924,14 @@ fn a_character_round_trips_between_two_partitions() { /// not "the file mentions it somewhere in a doc comment". /// /// `body_of`/`code_of` (comment-stripped) are the shared scan primitives in -/// [`crate::test_scan`] (issue #64 — this used to be six near-identical copies across this file +/// [`crate::test_scan`] (this used to be six near-identical copies across this file /// and five others, and they had already drifted: the first cut of /// `every_refuse_verdict_call_site_still_routes_through_the_by_guid_chokepoint` matched /// comments that EXPLAIN each fence rather than the fence itself, and a trailing (as opposed to /// whole-line) comment defeated the weak four-file version entirely). use crate::test_scan::{body_of, code_of, shape_of}; -/// Issue #64: this used to be the ONLY guard on `entity_by_owner`'s in-transit fence, and it is +/// This used to be the ONLY guard on `entity_by_owner`'s in-transit fence, and it is /// a source scan — it can prove the right identifiers appear, but not that they mean the right /// thing. The SENSE is now pinned directly (no `ReducerContext` needed) by /// `helpers::tests::gate_in_transit_refuses_an_in_transit_candidate_and_returns_a_normal_one`; @@ -939,7 +939,7 @@ use crate::test_scan::{body_of, code_of, shape_of}; /// never do. This test still earns its keep: it is what catches the fence being deleted, or its /// call to the gate being routed around, outright. /// -/// #380 collapsed all three chokepoints onto `helpers::gate_by_guid`, so the exact-shape STRING +/// Collapsed all three chokepoints onto `helpers::gate_by_guid`, so the exact-shape STRING /// PIN that used to sit next to this test (a whitespace-collapsed verbatim copy of /// `entity_by_owner`'s body, which every legitimate edit had to update twice) is gone: there is /// one two-line expression left where a `!` could hide, and it is inside `.cargo/mutants.toml`'s @@ -978,7 +978,7 @@ fn the_shared_gate_consults_the_ledger_and_defers_to_the_pure_decision() { #[test] fn player_login_still_refuses_an_in_transit_character() { - // #468 stage 4d: the fence lives in the shared login core — both the sender reducer and + // Stage 4d: the fence lives in the shared login core — both the sender reducer and // gw_player_login delegate there, so pinning the core covers both entries. let body = body_of( include_str!("../world.rs"), @@ -1008,15 +1008,15 @@ fn import_character_still_refuses_when_no_destination_copy_materialises() { } // ------------------------------------------------------------------------------------- -// ENFORCEMENT tripwires for the BY-GUID chokepoint (issue #30), one NAMED test per fenced -// path — the #26 review's lesson: deleting a fence must turn a test red, and a pure model +// ENFORCEMENT tripwires for the BY-GUID chokepoint, one NAMED test per fenced +// path — the review's lesson: deleting a fence must turn a test red, and a pure model // sees no reducers, so each of these is a source scan of the call site's own body. // ------------------------------------------------------------------------------------- /// The gate itself. Same shared decision as the actor chokepoint — all three route through /// `helpers::gate_by_guid` and thence `gate_in_transit`, so /// `gate_in_transit_refuses_an_in_transit_candidate_and_returns_a_normal_one` pins the SENSE of -/// all three at once (issue #64), and +/// all three at once, and /// `the_shared_gate_consults_the_ledger_and_defers_to_the_pure_decision` pins the one wrapper. /// This scan is what catches the fence being deleted, or routed around, outright. #[test] @@ -1036,7 +1036,7 @@ fn the_by_guid_chokepoint_still_calls_the_in_transit_gate() { /// Every REFUSE-verdict call site, one assertion each: `(file, fn signature, what breaks)`. /// Deleting the gate call from any one of them fails THIS test by name. /// -/// A sign inversion (issue #64) does NOT need re-checking at each of these ~12 sites: none of +/// A sign inversion does NOT need re-checking at each of these ~12 sites: none of /// them touches `is_in_transit` — they call `character_by_guid`/`character_by_name`, and the /// SENSE of those two now lives in exactly one place (`helpers::gate_in_transit`), pinned once /// by `the_by_guid_chokepoint_is_exactly_the_pinned_shape`. A sign flip anywhere in the fence @@ -1044,7 +1044,7 @@ fn the_by_guid_chokepoint_still_calls_the_in_transit_gate() { /// needs to prove each site still calls the (now sense-safe) wrapper at all. #[test] fn every_refuse_verdict_call_site_still_routes_through_the_by_guid_chokepoint() { - // #386 split `debug.rs` into the `debug/` directory; every `"debug.rs"` site below now + // Split `debug.rs` into the `debug/` directory; every `"debug.rs"` site below now // scans this concatenation-of-the-whole-directory blob instead of one `include_str!`. let debug_src = crate::test_scan::debug_dir_src(); let sites: &[(&str, &str, &str, &str)] = &[ @@ -1059,7 +1059,7 @@ fn every_refuse_verdict_call_site_still_routes_through_the_by_guid_chokepoint() ( "chat.rs", include_str!("../chat.rs"), - // #479 moved the body into the actor-explicit core; the fence travelled with it. + // Moved the body into the actor-explicit core; the fence travelled with it. "pub(crate) fn apply_send_whisper(", "apply_send_whisper reaches an in-transit character by NAME because begin_transfer \ persists with `set_offline: false`", @@ -1107,7 +1107,7 @@ fn every_refuse_verdict_call_site_still_routes_through_the_by_guid_chokepoint() "pub fn debug_reseed_skills(", "debug_reseed_skills writes game_player_skill, a HOT manifest table", ), - // --- Added by the #30 review's independent call-site audit. --- + // --- Added by the review's independent call-site audit. --- ( "world.rs", include_str!("../world.rs"), @@ -1140,7 +1140,7 @@ fn every_refuse_verdict_call_site_still_routes_through_the_by_guid_chokepoint() ]; for (file, src, signature, why) in sites { let body = code_of(src, signature); - // `require_character` (issue #371) is `character_by_guid(..).ok_or_else(..)` folded + // `require_character` is `character_by_guid(..).ok_or_else(..)` folded // into one helper — the fence is the SAME `character_by_guid` call, just no longer // spelled out at the caller, so it counts as routing through the chokepoint too. assert!( @@ -1154,7 +1154,7 @@ fn every_refuse_verdict_call_site_still_routes_through_the_by_guid_chokepoint() } } -/// The BACKGROUND writers, found by the #30 review's independent audit. Both are `game_tick_pass!` +/// The BACKGROUND writers, found by the review's independent audit. Both are `game_tick_pass!` /// bodies, so the argument that fences every other caller in their files — "the guid came from a /// live entity, and `begin_transfer` deleted it" — does not reach them: neither reads /// `game_world_entity` at all. They carry their own `is_in_transit` gate instead of routing @@ -1302,7 +1302,7 @@ fn owner_identity_is_regenerated_at_the_destination_never_carried() { `player_login`, so a carried copy arrives stale and is immediately overwritten — the \ REGENERATE verdict in this file's table (issue #30). Blob was:\n{blob}" ); - // #468 stage 4d: restamp lives in the shared login core (owner = ctx.sender() on the sender + // Stage 4d: restamp lives in the shared login core (owner = ctx.sender() on the sender // path, the account's bound identity on the gateway path — same regenerate semantics). let login = code_of( include_str!("../world.rs"), @@ -1331,7 +1331,7 @@ fn the_in_transit_predicate_is_the_login_fence_itself() { } // ------------------------------------------------------------------------------------- -// CROSS-DATABASE (issue #19): the transport ratchet, and the six-step crash matrix +// CROSS-DATABASE: the transport ratchet, and the six-step crash matrix // ------------------------------------------------------------------------------------- /// THE RATCHET. A character-owned table with no `character_owned!(transfer, ..)` arm does not @@ -1346,11 +1346,11 @@ fn the_in_transit_predicate_is_the_login_fence_itself() { /// recorded at the table is a different thing from an omission nobody noticed. /// /// Reads `crate::CHARACTER_OWNED_TRANSFER_NAMES`, the plain-string half of the generated -/// registry (#380). Deliberately not `crate::CHARACTER_OWNED_TRANSFERS` itself: referencing that +/// registry. Deliberately not `crate::CHARACTER_OWNED_TRANSFERS` itself: referencing that /// array materializes every registered fn's POINTER, which drags the SpacetimeDB host imports /// (`datastore_insert_bsatn`, `row_iter_bsatn_advance`, …) into this native test binary, which /// cannot link them. Same reasoning — and the same discovery-by-linker-error — as -/// `tripwires::build_scan_strip_tripwire::commented_out_markers_do_not_register`. Before #380 this test string-parsed the +/// `tripwires::build_scan_strip_tripwire::commented_out_markers_do_not_register`. Before this test string-parsed the /// generated Rust source to get the same list; build.rs emits it directly now. #[test] fn every_manifest_table_can_cross_a_database_boundary() { @@ -1400,7 +1400,7 @@ fn every_manifest_table_can_cross_a_database_boundary() { /// `sweep_transfer_game_item_instance` at `not_transported` left all 468 module tests green /// while deleting every character's entire inventory and equipped gear on every shard hop. /// -/// **The mechanism changed in #380.** "Transports" vs "declines" is no longer a property of an +/// **The mechanism changed.** "Transports" vs "declines" is no longer a property of an /// arm's BODY that a scanner has to read back out of the source — a transport arm has no body /// any more. It is the `character_owned!` marker KIND (`transfer` vs `not_transported`), which /// build.rs already parses, so the mechanical half of the decision arrives here as a generated @@ -1860,13 +1860,13 @@ fn the_arrival_copy_stays_fenced_until_the_source_copy_is_gone() { // `finish_destroys_the_source_copy_for_a_cross_database_transfer` and its ordering assertions // used to live here as a source scan of `fn do_finish(`. `do_finish` is now a two-line adapter -// over `apply_finish` (issue #34's seam), and every property that scan asserted — the cascade, +// over `apply_finish` (the seam), and every property that scan asserted — the cascade, // its `cross_database` gate, detach-before-cascade, cascade-before-record_shard — is executed // for real against `harness::FakeDb`. See `mod harness`'s `finish_*` tests. #[test] fn import_character_blob_still_proves_the_destination_copy_is_durable() { - // The reducer is now a two-line adapter over `apply_import_blob` (issue #37's seam), so the + // The reducer is now a two-line adapter over `apply_import_blob` (the seam), so the // guards below live in that body — but the ADAPTER still has to call it, and still has to // gate on the operator. Both, in order, or the whole harness is testing dead code. let shim = code_of(include_str!("mod.rs"), "pub fn import_character_blob("); @@ -1904,7 +1904,7 @@ fn import_character_blob_still_proves_the_destination_copy_is_durable() { importing on top of a resident character is the dual-liveness dupe with extra steps \ (and cross-database the source cannot see it). Body was:\n{body}" ); - // The MANIFEST-DRIFT guard — #16's contract, which the blob path inherited and which no + // The MANIFEST-DRIFT guard — the contract, which the blob path inherited and which no // test pinned (verified by mutation: deleting it left all 468 module tests green). Without // it a shard on a different build accepts a payload for a table set it does not have, and // silently drops whatever it cannot place. @@ -1925,7 +1925,7 @@ fn import_character_blob_still_proves_the_destination_copy_is_durable() { finish_transfer to cascade-delete the source copy the missing rows came from. Body \ was:\n{body}" ); - // #30's `defer_money_delta` residual: the blob's money is the escrowed value PLUS every + // the `defer_money_delta` residual: the blob's money is the escrowed value PLUS every // delta folded in after the freeze, and this assignment is the only thing that replays them // at the destination. Dropping it left all 468 module tests green. assert!( @@ -1954,13 +1954,13 @@ fn import_character_blob_still_proves_the_destination_copy_is_durable() { /// | `CtxShard::record_shard` → no-op | the source keeps no forwarding receipt | /// | `CtxShard::ensure_shadow_account` → no-op | the arriving player cannot log in at all | /// | `CtxShard::has_live_entity` → `false` | the dual-liveness dupe guard is dead | -/// | `CtxShard::bump_guid_high_water` → no-op | #59: this database can re-issue an imported character's guid | -/// | `CtxShard::own_guid_range` → `None` | #237's gate always reads "foreign"; a local arrival stops ratcheting | +/// | `CtxShard::bump_guid_high_water` → no-op | this database can re-issue an imported character's guid | +/// | `CtxShard::own_guid_range` → `None` | the gate always reads "foreign"; a local arrival stops ratcheting | /// | a `dest_y`/`dest_z` transposition in `begin_transfer`'s shim | the character arrives at the wrong place, and NOTHING else can see it | /// | `return Ok(())` above any shim's `apply_*` call | the reducer the gateway calls does nothing while every test passes | /// -/// **Why this pin survived #380 when the others did not.** The plan was to replace every -/// exact-shape pin with the cargo-mutants gate (`.cargo/mutants.toml`), and for the pins over +/// **Why this pin survived the mutation sweep when the others did not.** The plan was to replace +/// every exact-shape pin with the cargo-mutants gate (`.cargo/mutants.toml`), and for the pins over /// `begin_transfer`'s and `reap_transfers`' 120-line BODIES that is exactly what happened — /// those bodies are `apply_begin`/`apply_reap` now and the harness runs them, so the stringified /// twin that had to be edited alongside every legitimate change is gone. But cargo-mutants @@ -2105,7 +2105,7 @@ fn the_transport_arms_are_the_only_thing_that_moves_rows() { "import_rows no longer refuses an unknown table — it now silently drops its rows, and \ `finish_transfer` will destroy the source copy they came from. Body was:\n{body}" ); - // Issue #42 AC 2 — the refusal must be LOUD, naming the tables in the module log and not + // AC 2 — the refusal must be LOUD, naming the tables in the module log and not // only in the returned error. Nothing in this crate can capture SpacetimeDB's `log::error!`, // so this is a source scan: the behavioural half (refuse, name them, file no in-row) is // covered for real by `a_payload_missing_a_manifest_table_is_refused_and_files_no_in_row`, @@ -2130,7 +2130,7 @@ fn the_transport_arms_are_the_only_thing_that_moves_rows() { } // ------------------------------------------------------------------------------------- -// SCAN-PINNED, and why (issue #37 AC 5) +// SCAN-PINNED, and why (AC 5) // // Both guards below are a single call inside a reducer body, and their effect is only // observable in real table state — `teardown_instance_inner`'s row deletion and @@ -2168,7 +2168,7 @@ fn the_cross_database_eviction_keeps_the_instance_lease() { ); } -/// Issue #22 (group slice) DELETED the #19 interim group mirror — membership is realm-core's, +/// (group slice) DELETED the interim group mirror — membership is realm-core's, /// and the blob carrying a `begin_transfer` snapshot of it would race the authority (which is /// what made a party SPLIT across the boundary unable to see itself in the first place). /// diff --git a/module/src/transfer/transport.rs b/module/src/transfer/transport.rs index fa3cbf8a0..237ebaa88 100644 --- a/module/src/transfer/transport.rs +++ b/module/src/transfer/transport.rs @@ -6,7 +6,7 @@ //! codec underneath it, and [`ExportBlob`], the one value that crosses the wire. //! //! Everything here is `ReducerContext`-generic or `ReducerContext`-free, which is what lets -//! `harness.rs` execute the real export/import loops against a fake store (issue #37). [server] +//! `harness.rs` execute the real export/import loops against a fake store. [server] use spacetimedb::{log, ReducerContext, SpacetimeType}; @@ -18,7 +18,7 @@ use super::TransferOut; /// /// Deliberate simplification: in v1 the mark is CARRIED but not ACTED ON — one blob ships /// everything, because same-database transfers have nothing to stream. It becomes load-bearing -/// when the seam-crossing warm handoff has to fit a ~1s budget (spec #12, Phase C): cold tables +/// when the seam-crossing warm handoff has to fit a ~1s budget (spec, Phase C): cold tables /// move after the handshake. /// Verified against the generated enumeration by `hot_marks_name_only_real_manifest_tables`. pub(crate) const HOT_TABLES: &[&str] = &[ @@ -27,7 +27,7 @@ pub(crate) const HOT_TABLES: &[&str] = &[ "game_player_skill", "game_player_spell", "game_character_talent", - // Issue #72 hot-state audit: a buff/debuff bar (and Stealth, which is presence-only — no timer + // Hot-state audit: a buff/debuff bar (and Stealth, which is presence-only — no timer // to stream in "behind" anything) is exactly the first-frame-visible state this mark describes. "game_aura", ]; @@ -40,7 +40,7 @@ pub(crate) const MANIFEST_EXCLUDE: &[&str] = &["game_transfer_out"]; /// The CORE manifest tables whose rows deliberately do NOT cross a database boundary — the only /// core tables whose arm may be written with the `character_owned!(not_transported, ..)` marker -/// kind (issue #19 review). +/// kind (review). /// /// A Package's decline is not listed here and is not cross-checked against this list. This list /// exists so a core table's decision is reviewable in one place by everyone who reads the core; a @@ -70,9 +70,9 @@ pub(crate) const MANIFEST_EXCLUDE: &[&str] = &["game_transfer_out"]; /// - `game_group_invite` — a 2-minute dialog whose inviter is by definition not transferring. /// - `game_pet_command` — the live pet's stay/follow/aggressive state; the pet is a /// `game_world_entity`, which does not cross, so its command row has nothing to attach to. -/// - `game_group_member` — party membership (#22, group slice). Authoritative on REALM-CORE, so the +/// - `game_group_member` — party membership (group slice). Authoritative on REALM-CORE, so the /// blob must not carry it: a snapshot taken at `begin_transfer` would race the authority, and it -/// is exactly the snapshot #19's interim mirror was (a party SPLIT across the boundary could never +/// is exactly the snapshot the interim mirror was (a party SPLIT across the boundary could never /// see itself). The gateway re-pushes realm-core's roster onto the destination at world entry /// (`sync_group_mirror`), so membership crosses by replication rather than by carriage. /// - `game_mail_escrow` — a mail attachment in flight. The fence is a fact about the DATABASE that @@ -81,7 +81,7 @@ pub(crate) const MANIFEST_EXCLUDE: &[&str] = &["game_transfer_out"]; /// character hops, the fence stays, and the reaper there still judges it. /// - `game_mail_delivery` — the mail plane's delivery receipts. They only exist where the /// authoritative mail rows do, and no character transfers off realm-core. -/// - `game_character_shard` — the realm-core character→shard directory (#20). A routing HINT about +/// - `game_character_shard` — the realm-core character→shard directory. A routing HINT about /// where the character is, and the blob exists to change that: the snapshot `begin_transfer` takes /// still names the SOURCE, so carrying it would hand the destination a forwarding receipt pointing /// back at the shard the character just left. `do_finish` rewrites the source's own row to name the @@ -111,7 +111,7 @@ pub(crate) const NOT_TRANSPORTED: &[&str] = &[ "game_taxi_service_reply", // A Trade Session (+ its slot rows) is a live dialog with a partner who is by definition NOT // transferring too — carrying it would import a negotiation the destination's partner copy - // cannot see. It dies with the source, exactly as the logout teardown would (#120). + // cannot see. It dies with the source, exactly as the logout teardown would. "game_trade_session", "game_trade_slot", // A Duel and its one-shot relay rows are live state tied to two players on one shard. Moving @@ -131,7 +131,7 @@ pub struct ManifestEntry { pub hot: bool, } -/// One manifest table's ROWS, serialized (issue #19). `rows` is bsatn of that table's `Vec`, +/// One manifest table's ROWS, serialized. `rows` is bsatn of that table's `Vec`, /// produced and consumed by the table's own `character_owned!(transfer, ..)` arm — the only code /// that knows the row type. Everything between the two arms treats it as opaque bytes, which is /// what lets ONE blob carry every table with zero per-table code in the protocol itself. @@ -142,7 +142,7 @@ pub struct TableRows { } // =========================================================================================== -// Cross-database row transport (issue #19) +// Cross-database row transport // =========================================================================================== /// The direction a `character_owned!(transfer, ..)` arm is running in. ONE body serves both, so a @@ -169,7 +169,7 @@ pub enum RowIo<'a> { /// `C` is the context type. In production it is ALWAYS `ReducerContext` (inferred at every arm, so /// no arm changed shape when this parameter appeared) — it is generic solely so the execution /// harness in `mod harness` can drive this exact body against a fake store. See that -/// module's header for the seam and its ceiling (issue #37). +/// module's header for the seam and its ceiling. pub(crate) fn move_rows( ctx: &C, io: &mut RowIo<'_>, @@ -248,7 +248,7 @@ pub(crate) fn not_transported(io: &mut RowIo<'_>) { /// One entry of a transport registry: a table name and the `character_owned!(transfer, ..)` arm /// that moves its rows. `crate::CHARACTER_OWNED_TRANSFERS` is `&[TransportArm]`; -/// the harness supplies its own slice over a fake context (issue #37). +/// the harness supplies its own slice over a fake context. pub(crate) type TransportArm<'a, C> = (&'a str, fn(&C, u64, &mut RowIo<'_>)); /// Serialize every manifest table's rows for `character_guid` — the payload half of the export @@ -257,9 +257,9 @@ pub(crate) type TransportArm<'a, C> = (&'a str, fn(&C, u64, &mut RowIo<'_>)); /// /// The registry is a PARAMETER so this loop can be executed by `mod harness` — it is the /// only seam by which a test in this crate can run the real export body, since the arms themselves -/// need a `ReducerContext` (issue #37). +/// need a `ReducerContext`. /// -/// **No coverage check here, deliberately** (issue #42 AC 4). The import side needs one because it +/// **No coverage check here, deliberately** (AC 4). The import side needs one because it /// consumes a payload from ANOTHER database; this loop MANUFACTURES the payload from the same /// registry it would check against, pushing one entry per non-excluded arm unconditionally, so /// "the payload covers the registry" is a tautology no mutation of this body can break without @@ -299,12 +299,12 @@ pub(crate) fn export_rows(ctx: &ReducerContext, character_guid: u64) -> Vec Vec( ctx: &C, character_guid: u64, @@ -383,12 +383,12 @@ pub(crate) fn import_rows( /// /// The manifest is the load-bearing SCHEMA half: the destination compares it against its OWN build /// (`manifest()`) and refuses an import from a shard whose character-owned table set differs. The -/// `payload` alongside it is the DATA half (issue #19) — the actual rows, one entry per manifest +/// `payload` alongside it is the DATA half — the actual rows, one entry per manifest /// table, produced by that table's `character_owned!(transfer, ..)` arm. /// -/// **Nothing that is already inside `character_row` gets a second field here (#380).** Until then +/// **Nothing that is already inside `character_row` gets a second field here.** Until then /// the blob ALSO carried `name`/`level`/`map_id`/`instance_id`/`x`/`y`/`z`/`o`/`health`/`power` as -/// typed copies — pre-#19 remnants from when the blob was a manifest and there was no serialized +/// typed copies — earlier remnants from when the blob was a manifest and there was no serialized /// row to read them off. Not one of them was read by anything: `apply_import_blob` builds the /// arrival copy from `character_row` and overwrites the position from `dest_*`, `import_character` /// (same-database) reads only the manifest, and the gateway treats the whole blob as opaque bytes. @@ -396,7 +396,7 @@ pub(crate) fn import_rows( /// cannot tell which copy is authoritative — the same reasoning that keeps `owner_identity` out /// (see the REGENERATE verdict). `money` stays, and is the exception that shows the rule: it is the /// one field that is deliberately NOT just a copy of the row, because `defer_money_delta` folds -/// post-freeze credits into it (#30's DEFER verdict) and `apply_import_blob` replays it at the +/// post-freeze credits into it (the DEFER verdict) and `apply_import_blob` replays it at the /// destination. #[derive(SpacetimeType, Clone, Debug, PartialEq)] pub struct ExportBlob { @@ -406,7 +406,7 @@ pub struct ExportBlob { /// with `character_row`'s own `money` — that one is the value at `begin_transfer`. pub money: u32, pub manifest: Vec, - // --- issue #19: what makes the blob a real cross-DATABASE move rather than a manifest --- + // --- what makes the blob a real cross-DATABASE move rather than a manifest --- /// Where the character is going. Carried in the blob (not just the source's out-row) because /// cross-database the blob is the ONLY thing that reaches the destination. pub dest_map_id: u32, @@ -466,7 +466,7 @@ impl ExportBlob { } impl crate::character::Character { - /// Move the durable row to `dest` — the SIX fields an arrival overwrites, and no others (#380). + /// Move the durable row to `dest` — the SIX fields an arrival overwrites, and no others. /// /// Both halves of step 2 land here: same-database `import_character` re-partitions the shared /// row, cross-database `apply_import_blob` relocates the freshly decoded arrival copy. They used @@ -496,7 +496,7 @@ pub(crate) fn decode_blob(transfer_id: u64, bytes: &[u8]) -> Result Result<(), String> { let mine = manifest(); @@ -512,7 +512,7 @@ pub(crate) fn check_manifest(transfer_id: u64, arriving: &[ManifestEntry]) -> Re } /// Build the export blob for `character`. `ReducerContext`-free, so the harness can produce a REAL -/// blob from a fixture character and feed it to the REAL importer (issue #37) — the export half of +/// blob from a fixture character and feed it to the REAL importer — the export half of /// the round-trip property. `payload` comes from [`export_rows`], the only part that needs a /// database. pub(crate) fn build_export_blob( diff --git a/module/src/tripwires.rs b/module/src/tripwires.rs index 3bf117ae8..66d8788b7 100644 --- a/module/src/tripwires.rs +++ b/module/src/tripwires.rs @@ -1,21 +1,22 @@ -//! Source-scan tripwires (issue #379): `lib.rs` used to open with "this is the thin index" while +//! Source-scan tripwires: `lib.rs` used to open with "this is the thin index" while //! carrying 900+ lines of `#[cfg(test)]` scan machinery below its real index — pulled out here so //! that doc comment stays true. `#[cfg(test)] mod tripwires;` is `lib.rs`'s only mention of this //! file; every mod below is unchanged in what it enforces, only in where it lives. //! -//! Five tripwires, in file order: +//! Six tripwires, in file order: //! - [`character_owned_tripwire`] — every character-keyed table has a `character_owned` sweep marker. //! - [`build_scan_strip_tripwire`] — a commented-out marker invocation never registers. //! - [`partition_discipline_tripwire`] — no raw whole-table scan of a spatial table outside the -//! partition-scoped helpers, plus its #23 extension (no module game logic reads a shard id). +//! partition-scoped helpers, plus its extension (no module game logic reads a shard id). //! - [`character_fence_tripwire`] — no raw `game_character` lookup outside the by-guid chokepoint. -//! - [`print_macro_tripwire`] — no print-family macro anywhere under `module/src` (new with #432). -//! - [`gc_reap_tripwire`] — every TTL-shaped event table is named in `gc.rs` (new with #379). +//! - [`print_macro_tripwire`] — no print-family macro anywhere under `module/src` (new). +//! - [`gc_reap_tripwire`] — every TTL-shaped event table is named in `gc.rs` (new). +//! - [`issue_reference_tripwire`] — no comment carries a tracker issue reference. //! //! `partition_discipline_tripwire::raw_scans` and `character_fence_tripwire::raw_lookups` used to //! be ~70-line near-clones — identical bound-handle walk-back, handle dedup, comment-line filtering //! — that had already drifted from each other in small ways neither review caught. The exact drift -//! pattern issue #64 consolidated once already, one file down (`body_of`/`code_of` into +//! pattern consolidated once already, one file down (`body_of`/`code_of` into //! `test_scan.rs`). Both now defer to the one engine there, [`crate::test_scan::raw_table_reads`]: //! they differ only in which accessor(s) they watch and what counts as "opens a read" (`.iter()` / //! `.count()` for a whole-table scan vs `.guid().find(` for a raw point lookup), so the two can no @@ -64,7 +65,7 @@ pub(crate) mod character_owned_tripwire { /// them too). Shared with `partition_discipline_tripwire` below and `gc_reap_tripwire`. /// /// `pub(crate)`, not `pub(super)`: `creatures::tick`'s - /// `nothing_writes_the_unsubscribed_move_event_table` (#357) walks the whole compiled tree the + /// `nothing_writes_the_unsubscribed_move_event_table` walks the whole compiled tree the /// same way, from outside this module entirely. pub(crate) fn scanned_files() -> Vec { let manifest_dir = env!("CARGO_MANIFEST_DIR"); @@ -244,7 +245,7 @@ mod build_scan_strip_tripwire { } } -/// ENFORCEMENT tripwire (issue #14, part of the elastic-world-sharding spec #12): a full +/// ENFORCEMENT tripwire (part of the elastic-world-sharding spec): a full /// `.iter()` over a SPATIAL table reads the whole world, so it silently breaks shardability — /// once the world is cut into region/continent/instance shards, a whole-table scan can only ever /// see the rows on the caller's own shard, and the code that relied on seeing everything goes @@ -288,8 +289,8 @@ mod partition_discipline_tripwire { /// `(repo-relative path, allowed raw-scan count, why)`. One line of justification each. const WHITELIST: &[(&str, usize, &str)] = &[ - // Diagnostics and harness code — never on a gameplay path. #386 split the former single - // `debug.rs` (budget 9, after #368 dropped it from 12) into a directory; the 9 raw scans + // Diagnostics and harness code — never on a gameplay path. A split of the former single + // `debug.rs` (budget 9, down from 12) into a directory; the 9 raw scans // landed in two of the seven files — same total, just split along the new file boundary. ("module/src/debug/mod.rs", 9, "`debug_reducers`-gated test harness; compiled out of production builds entirely (2 of these are #456's `debug_backfill_cell_ids`, a deliberately whole-shard migration sweep — each shard runs it once after the publish that adds the `cell` column)"), ("module/src/debug/instance.rs", 4, "`debug_reducers`-gated test harness; compiled out of production builds entirely; +2 for #526's `debug_assert_floor_snap`, which scans game_creature_spawn/game_world_entity by `entry` for a guid high-water mark (template-keyed, not a spatial query) before minting a scratch spawn guid"), @@ -304,12 +305,12 @@ mod partition_discipline_tripwire { // Unindexed lookups whose target is co-located with the caller anyway. ("module/src/creatures/pet.rs", 1, "`nearest_hostile_near` — KNOWN DEBT, a textbook `entities_near` radius search kept only because pets are rare (one per online warlock)"), ("packages/deadmines/src/choreography.rs", 1, "single instance-scoped boss-liveness check inside one encounter"), - // Issue #383 split tick.rs into tick/{mod,movement,lifecycle,sense}.rs; this budget-5 entry + // Split tick.rs into tick/{mod,movement,lifecycle,sense}.rs; this budget-5 entry // splits with it, same total (3 + 2 = 5), same reasoning as the pre-split note below. ("module/src/creatures/tick/mod.rs", 3, "per-tick world sweep that locates every player (`active_cell_creatures`, feeding the pets/in_combat/active-cell sets) plus the 2 throttled work-item 230/233 rows-visited counters (`log_active_cell_stats`/`log_narrowed_pass_stats`) — perf-catalog Tier 1 took the OLD combined file from 10 to 7 (1.6/1.7/1.10), and the `timer_never` sentinel took corpse-decay and respawn-due (now in lifecycle.rs) off the list entirely"), ("module/src/creatures/cycle/ctx.rs", 2, "the aggro phase's sense-tick player snapshot (no players-only index) plus regeneration's health/power candidate read (no `hurt or has a power bar` index) — both moved here verbatim with their passes, the KNOWN DEBT tick/sense.rs used to carry, not new scans"), ("module/src/spell/cast/targeting.rs", 1, "AoE/chain target resolution by full-iter + squared distance; a textbook `helpers::entities_near` call (perf-catalog Tier 1)"), - // #368: both nearest-trainer scans now route through `helpers::nearest_entity` (an + // Both nearest-trainer scans now route through `helpers::nearest_entity` (an // indexed `by_map` scan, partition-scoped) — this file's raw-scan count dropped to 0, so // it no longer needs a whitelist entry. ]; @@ -330,7 +331,7 @@ mod partition_discipline_tripwire { /// accessor (`ctx.db.game_world_entity().iter()`) or off a local handle bound from it /// (`let entities = ctx.db.game_world_entity();` ... `entities.iter()`). Both forms count: /// the binding form is the one this codebase actually writes most of the time. Runs on - /// `crate::test_scan::raw_table_reads` (issue #379) — the shared engine this and + /// `crate::test_scan::raw_table_reads` — the shared engine this and /// `character_fence_tripwire::raw_lookups` used to duplicate. fn raw_scans(content: &str) -> Vec<(usize, &'static str)> { crate::test_scan::raw_table_reads(content, SPATIAL_ACCESSORS, opens_whole_table_read) @@ -412,12 +413,12 @@ mod partition_discipline_tripwire { } // ========================================================================================== - // #23 EXTENSION: no module game logic may read a SHARD ID. + // EXTENSION: no module game logic may read a SHARD ID. // ========================================================================================== /// The only files allowed to touch a shard column: the one that DEFINES the assignment table /// and the operator reducer that writes it (`region.rs`), and the one that DEFINES the - /// per-shard load-sample table and ITS operator reducer (`load.rs`, issue #78) — `game_shard_load + /// per-shard load-sample table and ITS operator reducer (`load.rs`) — `game_shard_load /// .shard` is a database-name LABEL an ops sample is filed under, ring-evicted and compared for /// equality, exactly like `RegionAssignment.shard`. Both write or compare; neither branches on /// the value to decide anything gameplay-visible. @@ -429,7 +430,7 @@ mod partition_discipline_tripwire { /// ever writing a dot (both forms were confirmed to slip past the first two). You cannot /// destructure a type you may not name. `game_character_shard` / `CharacterShard` deliberately /// do NOT count — that table stores a `(map_id, instance_id)` LOCATION precisely so nothing has - /// to name a database. `game_shard_load` / `ShardLoad` (issue #78) get the same destructuring + /// to name a database. `game_shard_load` / `ShardLoad` get the same destructuring /// protection as `RegionAssignment` — they carry a shard-name LABEL an ops sample is filed /// under, not a location, but the same "cannot destructure a type you may not name" argument /// applies to it too. @@ -465,7 +466,7 @@ mod partition_discipline_tripwire { out } - /// AC#1 of #23: **region definitions are data; shard ids are the gateway's business.** The + /// AC#1 of **region definitions are data; shard ids are the gateway's business.** The /// module stores `region_assignment { map_id, region_id, shard, epoch }` (it has to — realm-core /// is this same wasm under another database name), but the instant a reducer *branches* on /// `shard` the module stops being relocatable: the same code on two databases would take two @@ -475,7 +476,7 @@ mod partition_discipline_tripwire { /// `crates/lyracore-shared/src`**, which is compiled INTO this module and therefore just as capable /// of naming a database. /// - /// Issue #48 moved `DUNGEON_MAPS` out of `module/src/instance.rs` into `lyracore_shared::instance` so + /// Moved `DUNGEON_MAPS` out of `module/src/instance.rs` into `lyracore_shared::instance` so /// the gateway and the module could not disagree about which maps are dungeons. A map-id set is a /// world fact and entirely fine here — but the move quietly put a piece of module logic in a crate /// this tripwire did not look at, so "no module game logic reads a shard id" would have stopped @@ -545,12 +546,12 @@ mod partition_discipline_tripwire { } } -/// ENFORCEMENT tripwire (issue #30, part of the elastic-world-sharding spec #12): a reducer that +/// ENFORCEMENT tripwire (part of the elastic-world-sharding spec): a reducer that /// reaches a character by GUID or by NAME straight into `game_character` touches none of the /// escrowed-transfer chokepoints — not `helpers::entity_by_owner` (the actor side), not /// `world::player_login` (re-materialisation), not `begin_transfer`'s delete of the live entity /// (the target side). Same-database that is harmless: the write lands on the SAME row the -/// destination reads. Cross-database (#19) each one is a LOST WRITE, because the export blob was +/// destination reads. Cross-database each one is a LOST WRITE, because the export blob was /// serialized at `begin_transfer` and the mutation dies with the source copy. /// /// The fence is `helpers::character_by_guid` / `character_by_name`, which read an in-transit @@ -579,7 +580,7 @@ mod character_fence_tripwire { ("module/src/transfer/tests.rs", 1, "NOT a lookup: the expected-shape STRING LITERAL inside `the_production_adapter_is_the_pass_through_the_harness_assumes`, which quotes `CtxShard::has_character`'s body verbatim so any edit to it fails. This scanner reads text and is right to count it"), ("module/src/transfer/mod.rs", 4, "TRANSFER MACHINERY, all four reading the row the fence hides because that is the row they exist to move: `in_transit_instances` (the instance reaper's claim census — this call site exists BECAUSE the character is in transit, so a fenced reader would return None every time), `import_character`'s same-database re-partition, and `CtxShard`'s two sink methods — `has_character` (the plan probe, the materialise upsert AND the post-import durability PROOF that the destination copy exists before an in-row licenses deleting the source) and `with_character` (the escrow's own export read, taken after the freeze). #380 dropped this from 8 to 4: the other four were expected-shape STRING LITERALS inside exact-shape pins that quoted `CtxImportSink`'s and `helpers.rs`'s bodies verbatim. The `helpers.rs` ones are gone (all three chokepoints collapsed onto one `gate_by_guid`, whose mutants cargo-mutants CATCHES); the adapter's moved to `transfer/tests.rs`, which is why that file now has a budget of its own"), ("module/src/world.rs", 8, "player_login carries its own `is_in_transit` fence (the re-materialisation chokepoint, 2 reads); teleport_player, set_home, persist_entity and is_gm_character take a guid already resolved through a fenced entity (persist_entity is called BY begin_transfer); debug_delete_character keeps a raw existence probe so a missing character stays a no-op while an in-transit one is REFUSED, with the fence on the same expression; cascade_delete_character's raw row DELETE is the sweep that probe guards. `recall_to_home` is now FENCED (character_by_guid) — it was the one teleport_player caller needing no live entity"), - // OPEN, not decided — spec #12 puts group MEMBERSHIP state on realm-core, settled by #22. + // OPEN, not decided — spec puts group MEMBERSHIP state on realm-core, settled. ("module/src/group.rs", 6, "OPEN: group_accept/group_uninvite/group_leave have a THIRD party mutate game_group_member; if #22 lands membership on realm-core these stop being a world shard's concern entirely. Guessing a verdict here is the mistake #30 exists to correct"), // REGENERATE at the destination — connection-derived state, never carried in the blob. ("module/src/auth.rs", 4, "REGENERATE: create_character's two name checks (NAME_IN_USE, pre-insert and the race-losing retry) predate any character; its guid-allocator seed scan (`legacy_guid_seed_now`, first-ever touch only) needs the whole table; delete_character keeps a raw find so NO_SUCH_CHAR/NOT_OWNER/CHAR_IN_TRANSIT stay three answers, with the fence on the next line. establish_session's owner_identity rebind and create_character's per-account cap check now route through the `by_account` index instead of a full scan (issue #390), so they no longer count here"), @@ -595,8 +596,8 @@ mod character_fence_tripwire { ("module/src/talent.rs", 1, "the guid comes from the caller's own entity_by_owner-resolved entity"), ("module/src/gm.rs", 2, "set_gm_level is FENCED (character_by_name); the two raw reads are gm_command's own gm_level probe and its .money write, both on the caller's entity_by_owner-resolved guid"), ("module/src/loot/mod.rs", 1, "DEFER: credit_purse writes the durable row AFTER folding the delta into the escrowed blob (`transfer::defer_money_delta`) — refusing would drop a third party's copper"), - // #386 split the former single `debug.rs` into a directory; this lookup lives in the one - // reducer the #378 collapse put it in. + // Split the former single `debug.rs` into a directory; this lookup lives in the one + // reducer the collapse put it in. ("module/src/debug/repair.rs", 1, "`debug_repair_after_publish`'s gm-tester backfill (guid 1, formerly the standalone `debug_seed_gm_tester`); every debug WRITER that touches character state is fenced"), ("packages/playerbots/src/goals.rs", 1, "a bot reads a nearby player's NAME for goal selection — no write"), ("packages/playerbots/src/mod.rs", 4, "bot roster bookkeeping over rows this same reducer just created — free-name probe, post-create fetch, post-update re-read; +1 for `ensure_bot_account`'s per-account character COUNT, which reaches no character (it decides which bot account still has room under the 10-character cap) — an in-transit bot counting or not counting toward that cap is harmless either way"), @@ -633,7 +634,7 @@ mod character_fence_tripwire { /// Every line where `game_character` is read raw — inline off the accessor, or off a local /// handle bound from it (`let chars = ctx.db.game_character();`, this codebase's dominant idiom). - /// Runs on `crate::test_scan::raw_table_reads` (issue #379) — the shared engine this and + /// Runs on `crate::test_scan::raw_table_reads` — the shared engine this and /// `partition_discipline_tripwire::raw_scans` used to duplicate. fn raw_lookups(content: &str) -> Vec { let mut out: Vec = @@ -726,10 +727,10 @@ mod character_fence_tripwire { } } -/// ENFORCEMENT tripwire (issue #432): SpacetimeDB's own CLI (2.7.1) source-scans this whole crate +/// ENFORCEMENT tripwire: SpacetimeDB's own CLI (2.7.1) source-scans this whole crate /// for the print family — four macro names, each followed by a `!` — and aborts the build the /// instant one appears ANYWHERE under `module/src`, including behind `#[cfg(test)]`: the scan is -/// source-level and does not know what `cfg` means. #432 was three diagnostic notes in test-only +/// source-level and does not know what `cfg` means. The trigger was three diagnostic notes in test-only /// code (`publish_safety.rs` ×2, `test_scan.rs` ×1) that never compile into the wasm at all, yet /// still failed every fresh clone's first `preflight` — the alpha's own start command, blocked for /// everyone before a single reducer ever ran. Fixed there by swapping each to @@ -786,7 +787,7 @@ pub(crate) mod print_macro_tripwire { } } -/// ENFORCEMENT tripwire (issue #379): `gc.rs` has a hand-maintained `reap!` list. It covers the +/// ENFORCEMENT tripwire: `gc.rs` has a hand-maintained `reap!` list. It covers the /// fifteen event tables reaped on their 1s `EVENT_TTL_MICROS` and the handful of tables with their /// own ad-hoc TTL block below it. A NEW short-lived event table /// that forgets to add its line leaks forever, silently: nothing else ever deletes its rows. @@ -870,7 +871,7 @@ mod gc_reap_tripwire { } // ================================================================================================= -// #456: every `grid_x` write is accompanied by the packed `cell` write +// Every `grid_x` write is accompanied by the packed `cell` write // ================================================================================================= /// The four AOI-scoped tables carry a `cell` column that packs `(grid_x, grid_y)` into one indexed @@ -881,7 +882,7 @@ mod gc_reap_tripwire { /// every player standing on it and visible to whoever happens to occupy the cell it wrongly claims. /// /// Nothing in the type system couples them: `grid_x`, `grid_y` and `cell` are three independent -/// columns, and the census behind #456 found **24 independent write sites** across 8 files, with no +/// columns, and the census behind this tripwire found **24 independent write sites** across 8 files, with no /// shared constructor for `game_world_entity` re-stamps (7 hand-rolled `e.grid_x = ..` runs) or for /// `game_gameobject` (9 independent struct literals). Adding a 25th and forgetting the third line /// compiles clean, passes every existing test, and shows up live as entities that vanish. @@ -1196,7 +1197,7 @@ pub(crate) mod grid_cell_tripwire { ); } - /// Issue #467: the tripwire's original `grid_x: ` needle never saw field-init SHORTHAND + /// The tripwire's original `grid_x: ` needle never saw field-init SHORTHAND /// (`grid_x,`, no `: value`) — what an initializer built from same-named locals uses, the /// idiom `motion.rs` uses throughout. This feeds `grid_x_writes_missing_cell` a struct literal /// written that way and asserts a broken one (no `cell` field) is CAUGHT, a correct one PASSES, @@ -1244,7 +1245,7 @@ pub(crate) mod grid_cell_tripwire { ); } - /// A code-review finding on #467: the shorthand path resolved its enclosing struct-literal type + /// A code-review finding on the shorthand path resolved its enclosing struct-literal type /// by searching the file for `struct { .. }`, which finds nothing for the near-universal /// `Self { .. }` constructor idiom (`impl E { fn f() -> Self { Self { .. } } }`) — `Self` is not /// a struct declaration. Confirms a broken `Self { .. }` shorthand initializer is still CAUGHT. @@ -1266,7 +1267,7 @@ pub(crate) mod grid_cell_tripwire { ); } - /// A code-review finding on #467: `is_own_line` required the ENTIRE trimmed line to be exactly + /// A code-review finding on `is_own_line` required the ENTIRE trimmed line to be exactly /// `grid_x,`, so a compact one-liner (`E { grid_x, grid_y }`, multiple fields sharing a line) /// was skipped outright — violation or not. Confirms a broken compact literal is still CAUGHT. #[test] @@ -1287,3 +1288,84 @@ pub(crate) mod grid_cell_tripwire { ); } } + +/// ENFORCEMENT tripwire: a comment must stay understandable without tracker history, so no comment +/// under `module/src` may carry an issue reference (a hash sign followed by 2..=4 digits). The +/// allow-list below is a RATCHET of MAXIMUMS: cleaning a file up lowers its number, and a file that +/// reaches zero drops off the list entirely. +#[cfg(test)] +pub(crate) mod issue_reference_tripwire { + /// `(path under `module/src`, tracker references still tolerated there)`. + const RATCHET: &[(&str, usize)] = &[("spell/cast/targeting.rs", 6)]; + + /// Lines whose comment carries a tracker reference: a hash sign opening a 2..=4 digit token + /// after a space or an opening bracket. A build, opcode, patch or revision number is not one. + fn tracker_refs(content: &str) -> Vec { + let bytes = content.as_bytes(); + let mut out = Vec::new(); + for (i, _) in content.match_indices('#') { + let digits = bytes[i + 1..] + .iter() + .take_while(|c| c.is_ascii_digit()) + .count(); + if !(2..=4).contains(&digits) || i == 0 || !matches!(bytes[i - 1], b' ' | b'(' | b'[') { + continue; + } + if !crate::test_scan::on_comment_line(content, i) { + continue; + } + let head = content[..i - 1].trim_end(); + let word = &head[head.rfind(char::is_whitespace).map_or(0, |j| j + 1)..]; + if matches!(word, "build" | "opcode" | "patch" | "rev") { + continue; + } + out.push(crate::test_scan::line_of(content, i)); + } + out + } + + #[test] + fn no_comment_carries_a_tracker_issue_reference() { + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut files = Vec::new(); + super::character_owned_tripwire::collect_rs_files(&root, &mut files); + let mut violations = Vec::new(); + for file in files { + let rel = file + .strip_prefix(&root) + .expect("scanned under module/src") + .to_string_lossy() + .replace('\\', "/"); + let content = std::fs::read_to_string(&file).expect("readable module source"); + let lines = tracker_refs(&content); + let budget = RATCHET + .iter() + .find(|(p, _)| *p == rel) + .map_or(0, |(_, n)| *n); + if lines.len() > budget { + let found = lines.len(); + violations.push(format!( + "{rel}: {found} refs (budget {budget}), lines {lines:?}" + )); + } + } + assert!( + violations.is_empty(), + "tracker issue references found in module/src comments:\n {}\n\n\ + Record the reasoning in the comment itself: a reader with no tracker access must still \ + understand why the constraint exists.", + violations.join("\n ") + ); + } + + /// The scan catches a reference and leaves a build number, an opcode id and live code alone. + #[test] + fn the_scan_separates_a_tracker_reference_from_an_ordinary_number() { + let h = '#'; + assert_eq!(tracker_refs(&format!("// a note ({h}119)\n")), vec![1]); + assert_eq!(tracker_refs(&format!("/// see {h}42 for why\n")), vec![1]); + assert!(tracker_refs(&format!("// build {h}5875 ships it\n")).is_empty()); + assert!(tracker_refs(&format!("// opcode {h}117 is CMSG\n")).is_empty()); + assert!(tracker_refs(&format!("let s = \"({h}119)\";\n")).is_empty()); + } +} diff --git a/module/src/vmap.rs b/module/src/vmap.rs index 586f63854..5afaed351 100644 --- a/module/src/vmap.rs +++ b/module/src/vmap.rs @@ -1,5 +1,5 @@ -//! Exact per-cell collision-triangle store + ray queries (issue #521, part of the #169 full-vmap -//! epic; design record `docs/decisions.md` §10). Builds on #520's codec/binning +//! Exact per-cell collision-triangle store + ray queries (part of the full-vmap +//! epic; design record `docs/decisions.md` §10). Builds on the codec/binning //! (`lyracore_shared::vmap`): this slice adds the module-PRIVATE table the packed per-cell blobs //! land in, the import reducers that fill it (mirroring `nav::import_nav_chunks`/`_append`), and //! the module-side wrappers over `lyracore_shared::vmap::cast_ray` — LoS (WMO-class triangles @@ -454,7 +454,7 @@ pub fn discard_vmap_generation(ctx: &ReducerContext, generation_id: u64) -> Resu } // =========================================================================================== -// Nav coverage derivation (issue #195) — per-cell walk/obstruction blobs derived module-side +// Nav coverage derivation — per-cell walk/obstruction blobs derived module-side // from a generation's OWN staged `VmapGenerationChunk` rows, never a re-import and never // `game_nav_chunk` (that table has no generation column and is cleared wholesale by the `--nav` // import pipeline). Coverage is generation-scoped so it can never half-exist: it belongs to @@ -748,7 +748,7 @@ pub fn import_vmap_chunks_append(ctx: &ReducerContext, packed: String) -> Result } // =========================================================================================== -// Runtime consumption (#521) — module-side wrappers over `lyracore_shared::vmap::cast_ray`, +// Runtime consumption — module-side wrappers over `lyracore_shared::vmap::cast_ray`, // gated on `game_config.vmap_enabled`. // =========================================================================================== @@ -846,7 +846,7 @@ pub fn probe_rays( } // =========================================================================================== -// Model floor heights (#526) — a down-ray probe over the same collision-class triangle store, +// Model floor heights — a down-ray probe over the same collision-class triangle store, // so creature Z-placement/movement can stand on model floors (bridges, WMO interiors like // Deadmines' decks) that terrain's heightmap knows nothing about (`terrain::ground_z` only // samples the ADT MCVT grid — the walkable surface UNDER a bridge, not the deck itself). @@ -889,7 +889,7 @@ pub fn probe_floor_z( } // =========================================================================================== -// Indoor/outdoor area-info (#527) — mangos parity: `VMapManager2::getAreaInfo` / the indoor +// Indoor/outdoor area-info — mangos parity: `VMapManager2::getAreaInfo` / the indoor // half of `Player::CheckAreaExploreAndOutdoor`. Same down-ray probe shape as `floor_z`, but // WMO-only (a doodad standing in an open field doesn't make the point "indoor") and reporting // the containing group's id + MOGP indoor bit instead of just the hit height. @@ -918,12 +918,12 @@ pub fn area_info( } // =========================================================================================== -// Per-cell indoor presence (issue #22) — a derived, module-private pre-reject in front of +// Per-cell indoor presence — a derived, module-private pre-reject in front of // `area_info`, so a movement heartbeat can ask "am I indoors?" every 100 ms without paying a // ray cast anywhere the map has no interior geometry at all (which is nearly everywhere). // // Kept in one region at the END of this file on purpose: the generation lifecycle above is -// contested ground (issue #195's coverage derivation lands there too), and everything here is +// contested ground (the coverage derivation lands there too), and everything here is // additive — one table, one marker write inside `verify_vmap_generation`'s existing decode // pass, one indexed find on the read path. // =========================================================================================== diff --git a/module/src/world.rs b/module/src/world.rs index 1ebc9bc85..313e85f00 100644 --- a/module/src/world.rs +++ b/module/src/world.rs @@ -1,6 +1,6 @@ //! The live in-world player: the field-sync `WorldEntity` row (also reused for creatures, type //! Unit), the per-recipient movement-relay event table, and the login/movement/death cores the -//! `gw::gw_*` reducers drive (#483 — the sender-path twins are gone) plus `on_disconnect`. +//! `gw::gw_*` reducers drive (the sender-path twins are gone) plus `on_disconnect`. //! [entity]/[event] use lyracore_shared::constants::sheath_state; @@ -8,7 +8,7 @@ use lyracore_shared::spatial; use spacetimedb::{reducer, table, Identity, ReducerContext, Table}; use crate::faction::game_faction_template; -// Graveyard resolution (work-item 209/226) lives in `graveyard.rs` (issue #385 extraction) — this +// Graveyard resolution (work-item 209/226) lives in `graveyard.rs` (extraction) — this // alias keeps every `graveyard::...` call site below byte-identical. use crate::graveyard; use crate::helpers::entity_by_owner; @@ -100,7 +100,7 @@ pub(crate) fn clear_relay_world_states_for_instance(ctx: &ReducerContext, instan public, index(accessor = by_map, btree(columns = [map_id])), index(accessor = by_grid, btree(columns = [map_id, instance_id, grid_x, grid_y])), - // #456: the AOI subscription's index. Exactly 3 columns, all matched by equality terms — + // The AOI subscription's index. Exactly 3 columns, all matched by equality terms — // the only shape SpacetimeDB 2.7.1's subscription planner can serve (see the `cell` column). // `by_grid` stays: the MODULE reaches it through the generated index accessor, not SQL, so // the 3-column planner limit does not apply to `helpers::entities_near`. @@ -336,7 +336,7 @@ pub struct WorldEntity { /// bool itself — the rest byte ships via PLAYER_BYTES_2 in `game_rest_state_event`). #[default(false)] pub resting: bool, - /// #456: `(grid_x, grid_y)` packed into ONE indexed value — the AOI subscription's cell key. + /// `(grid_x, grid_y)` packed into ONE indexed value — the AOI subscription's cell key. /// /// SpacetimeDB 2.7.1's subscription planner can only serve a query from an index when EVERY /// column of that index is matched by an equality term, and it skips any index with more than 3 @@ -359,7 +359,7 @@ pub struct WorldEntity { /// post-publish step this migration REQUIRES. #[default(0i64)] pub cell: i64, - /// Character-sheet numbers (#517): `spell::recompute_sheet` is the SINGLE chokepoint that writes + /// Character-sheet numbers: `spell::recompute_sheet` is the SINGLE chokepoint that writes /// these sheet fields (base + `A_MOD_STAT`/`A_MOD_COMBAT(ATTACK_POWER)` aura + equipped gear incl. /// enchants — the exact same folds `combat::swing_range_ctx` rolls against), so the gateway's /// `build_sheet_stats_values` is a plain row read, never a second copy of aura/gear semantics. A @@ -386,7 +386,7 @@ pub struct WorldEntity { pub sheet_dmg_min: u32, #[default(0)] pub sheet_dmg_max: u32, - /// Melee crit chance in basis points (#532) — a plain copy of `combat::effective_crit_bp`'s + /// Melee crit chance in basis points — a plain copy of `combat::effective_crit_bp`'s /// output, the SAME fold the swing table rolls against (flat base + agility-derived, /// level-suppressed + gear crit rating + `A_MOD_COMBAT(CRIT)` auras). No second formula: this /// column exists only so the gateway can relay `PLAYER_CRIT_PERCENTAGE` without recomputing crit @@ -394,7 +394,7 @@ pub struct WorldEntity { /// so `publish` auto-migrates. #[default(0)] pub sheet_crit_bp: u32, - /// `UNIT_FIELD_BYTES_2` (#101): byte 0 is the SHEATH STATE — 0 = weapons stowed, 1 = melee drawn, + /// `UNIT_FIELD_BYTES_2`: byte 0 is the SHEATH STATE — 0 = weapons stowed, 1 = melee drawn, /// 2 = ranged drawn. Written only by `set_sheathed` (the `CMSG_SETSHEATHED` the client sends on /// `Z`), read by the gateway create block + relay so PEERS see a weapon drawn or stowed at all. /// Distinct from `unit_bytes_1` (stand state / shapeshift / ghost vis) and from `player_bytes_2` @@ -466,7 +466,7 @@ impl WorldEntity { /// **The per-mover motion row (perf catalog 2.1).** One row per moving entity, UPDATED IN PLACE, /// carrying the same `(opcode, movement_info)` payload the old per-recipient `game_movement_event` -/// relay table used to carry (dropped — #350; nothing wrote it any more after this table replaced +/// relay table used to carry (dropped; nothing wrote it any more after this table replaced /// it). /// /// Why it exists: the old table inserted one row PER NEARBY PLAYER per movement, so a crowd cost @@ -489,7 +489,7 @@ impl WorldEntity { accessor = game_entity_motion, public, index(accessor = by_grid, btree(columns = [map_id, instance_id, grid_x, grid_y])), - // #456: the AOI cell index — see the `cell` column's doc comment. + // The AOI cell index — see the `cell` column's doc comment. index(accessor = by_cell, btree(columns = [map_id, instance_id, cell])) )] pub struct EntityMotion { @@ -504,7 +504,7 @@ pub struct EntityMotion { pub opcode: u16, pub movement_info: Vec, pub seq: u32, - /// #456: `(grid_x, grid_y)` packed into ONE indexed value — the AOI subscription's cell key. + /// `(grid_x, grid_y)` packed into ONE indexed value — the AOI subscription's cell key. /// /// SpacetimeDB 2.7.1's subscription planner can only serve a query from an index when EVERY /// column of that index is matched by an equality term, and it skips any index with more than 3 @@ -579,7 +579,7 @@ fn is_gm_character(ctx: &ReducerContext, guid: u64) -> bool { } /// One heartbeat's raw position/time delta since the mover's last PERSISTED heartbeat — the anti-cheat -/// scorer's whole input, and what `plan_movement` derives `moved` from. Issue #385: bundled instead of +/// scorer's whole input, and what `plan_movement` derives `moved` from. Bundled instead of /// 7 loose positional floats/u32s (`score_and_log_movement` used to take 9 arguments counting /// `ctx`/`guid`; `debug_score_movement` builds one of these from its own flat wire args to call it). pub(crate) struct MovementDelta { @@ -657,7 +657,7 @@ pub(crate) fn score_and_log_movement(ctx: &ReducerContext, guid: u64, delta: &Mo /// A logged movement-plausibility anomaly (255). One row per flagged delta; the flag is advisory (the /// move was NOT rejected). Not `public` — server-internal until the GM console (205) surfaces it. These /// are recent diagnostics, reaped after the shared `EVENT_TTL_MICROS` window so a benchmark's intentional -/// speeders cannot grow the table without bound (issue #211). Query a live character with +/// speeders cannot grow the table without bound. Query a live character with /// `SELECT * FROM game_movement_violation WHERE guid = :guid`; durable forensics belongs in reducer logs. #[table(accessor = game_movement_violation)] pub struct MovementViolation { @@ -796,7 +796,7 @@ pub(crate) fn teleport_player( if e.map_id != map_id || e.instance_id != instance_id { crate::duel::interrupt_duel_for(ctx, player_guid); } - // #461: a movement packet staged before the teleport describes the OLD position. Left queued, the + // A movement packet staged before the teleport describes the OLD position. Left queued, the // next `publish_motion` firing would relay it up to a tick AFTER the teleport landed and snap // every nearby peer's view of this player back to where they were. Drop it in both branches — // the authoritative position is the destination, and the player's next heartbeat relays it. @@ -899,7 +899,7 @@ pub(crate) fn set_home(ctx: &ReducerContext, guid: u64) { /// Recall a character to its hearthstone home — an IMMEDIATE teleport via the shared core (the vanilla /// ~10s channel/cast is a follow-up). No-op if the character row is gone. [entity] /// -/// REFUSE verdict (issue #30). This is the ONE `teleport_player` caller that needs no live entity — +/// REFUSE verdict. This is the ONE `teleport_player` caller that needs no live entity — /// it resolves the home coords straight off the durable row — so it is the only route by which /// `teleport_player`'s unconditional durable-row write (map_id/x/y/z/orientation/pending_instance_id, /// FIVE `ExportBlob` fields plus the id `in_transit_instances` reads) can land on an escrowed @@ -926,10 +926,10 @@ pub(crate) fn recall_to_home(ctx: &ReducerContext, guid: u64) { // Enter world // =========================================================================================== -/// The login core, actor-explicit (#468 stage 4d): everything the old sender-path `player_login` +/// The login core, actor-explicit (stage 4d): everything the old sender-path `player_login` /// did after resolving WHOSE login this is. `owner` is the identity stamped onto the live entity /// and the character's owner-RLS rows — on the gateway path (`gw::gw_player_login`, the only -/// remaining caller, #483) the account's BOUND identity, so the rows a per-player connection would +/// remaining caller) the account's BOUND identity, so the rows a per-player connection would /// see stay owned by the identity that connection would present. pub(crate) fn apply_player_login( ctx: &ReducerContext, @@ -946,7 +946,7 @@ pub(crate) fn apply_player_login( return Err("character does not belong to caller".to_string()); } - // IN-TRANSIT FENCE (issue #16): `begin_transfer` deleted this character's live entity, so every + // IN-TRANSIT FENCE: `begin_transfer` deleted this character's live entity, so every // targeting/aggro/threat/AOI gate already cannot see it — login is the one path that could // materialise it again, which on a shard the character has left is exactly the dual-liveness // dupe the escrow exists to prevent. Refuse until `finish_transfer` (or the reaper) clears the @@ -1177,7 +1177,7 @@ fn restamp_owned_data(ctx: &ReducerContext, character_guid: u64, identity: Ident /// combat/event rows (auras, combo points, lockouts, pending casts) are exempt from all of this — /// they die with the live entity/session, not the durable character. [entity] pub(crate) fn cascade_delete_character(ctx: &ReducerContext, character_guid: u64) { - // Issue #59 defect 1: floor the guid allocator at `character_guid` BEFORE it disappears, on + // Defect 1: floor the guid allocator at `character_guid` BEFORE it disappears, on // EVERY delete path (this is the one chokepoint all of them share — transfer, CMSG_CHAR_DELETE, // debug_delete_character). A lazily-seeded allocator that has never been touched yet seeds // itself from a scan of the SURVIVING rows the moment `next_character_guid` first runs — which, @@ -1196,7 +1196,7 @@ pub(crate) fn cascade_delete_character(ctx: &ReducerContext, character_guid: u64 // resurrection sickness (live find, 2026-07-19). // // Auras ON the character used to be hand-deleted here too, for the identical reason - // (`target_guid` is not one of the tripwire's magic field names). Issue #72's hot-state audit + // (`target_guid` is not one of the tripwire's magic field names). The hot-state audit // gave `game_aura` a real `character_owned!` marker instead (`sweep_delete_game_aura` / // `sweep_transfer_game_aura`, `spell/tables.rs`) — the delete half above already runs it via // `CHARACTER_OWNED_DELETE_SWEEPS`, and the marker is also what lets a warm handoff carry the @@ -1221,7 +1221,7 @@ pub(crate) fn cascade_delete_character(ctx: &ReducerContext, character_guid: u64 // never be re-armed, and nothing ever disengaged it. Despawning 300 bots orphaned ~100 creatures // this way (live, 2026-07-29) and they piled onto the only player left standing. // - // Issue #365: this used to hand-roll `combat::disengage` inline (delete the outgoing row, free + // This used to hand-roll `combat::disengage` inline (delete the outgoing row, free // attackers via `by_target`, clear_target each, `threat::clear_for_unit`) — the same three steps // the logout path (below) and the cross-map teleport path call the real helper for. The hand-roll // had drifted: `disengage` ALSO drops IN_COMBAT (+ zeroes `combat_until_ms`) on any attacker left @@ -1233,7 +1233,7 @@ pub(crate) fn cascade_delete_character(ctx: &ReducerContext, character_guid: u64 .game_corpse() .guid() .delete(crate::corpse::corpse_guid_for(character_guid)); - crate::motion::drop_pending(ctx, character_guid); // #461: staged-but-unpublished motion too + crate::motion::drop_pending(ctx, character_guid); // Staged-but-unpublished motion too ctx.db.game_entity_motion().guid().delete(character_guid); // see the despawn path ctx.db.game_world_entity().guid().delete(character_guid); ctx.db.game_character().guid().delete(character_guid); @@ -1243,7 +1243,7 @@ pub(crate) fn cascade_delete_character(ctx: &ReducerContext, character_guid: u64 /// `cascade_delete_character` behind an ownership check; this debug entry is otherwise gate-free for /// the harness. /// -/// REFUSE verdict (issue #30) — the SAME verdict `auth::delete_character` carries, for the same +/// REFUSE verdict — the SAME verdict `auth::delete_character` carries, for the same /// reason: this destroys a durable copy another shard holds a claim on. It is strictly worse here /// than there, because `cascade_delete_character` runs the `character_owned!` sweep, which includes /// `sweep_delete_game_transfer_out` — so an unfenced call deletes the character AND both escrow rows @@ -1271,7 +1271,7 @@ pub fn debug_delete_character(ctx: &ReducerContext, character_guid: u64) -> Resu // Movement: persist + relay // =========================================================================================== -/// #110 ground truth. The gateway reports submitting ~400 movement calls/s at 200 players and +/// Ground truth. The gateway reports submitting ~400 movement calls/s at 200 players and /// receiving a completion for every one, while `spacetime_num_txns_total{reducer="movement_update"}` /// reports ~200/s. Exactly one of those is wrong, and no amount of gateway-side instrumentation can /// say which — this counts ENTRIES TO THE REDUCER ITSELF, which is the only unambiguous witness. @@ -1325,7 +1325,7 @@ pub(crate) fn snapshot_needs_persist( || drift_sq > PERSIST_MAX_DRIFT_YD * PERSIST_MAX_DRIFT_YD } -/// Environmental damage, absorbed out of `movement_update`'s own inline block (issue #385): given the +/// Environmental damage, absorbed out of `movement_update`'s own inline block: given the /// shared curve's damage figure (`lyracore_shared::env::fall_damage`, already computed by the caller /// from the client's airborne time + max_health) and the mover's CURRENT health, decide the health to /// carry forward and whether the landing is lethal. A lethal fall does NOT subtract here — the @@ -1349,10 +1349,10 @@ pub(crate) fn resolve_environmental_damage(dmg: u32, health: u32) -> (u32, bool) /// already run against `mover`. From here on the reducer does nothing but execute this: no further /// branching on raw movement state happens after `plan_movement` returns. /// -/// Issue #385: this replaces the reducer's own formatted source as what the "relay is never gated on +/// This replaces the reducer's own formatted source as what the "relay is never gated on /// the persist decision" invariant pins against — a whitespace-collapsed exact-body match that broke /// on every rename or rustfmt re-wrap. `relay_motion` is unconditionally `true` (see `plan_movement`'s -/// doc) regardless of every other field, which is exactly the shape `#109` regressed on once (only the +/// doc) regardless of every other field, which is exactly the shape that regressed once (only the /// entity row gated; the per-mover motion relay never is) — and it is now an ordinary value-level unit /// test (`plan_movement`'s tests, below) instead of a source scan. pub(crate) struct MovementPlan { @@ -1411,7 +1411,7 @@ pub(crate) fn plan_movement( } } -/// The movement core, actor-explicit (#468 stage 4): everything the old sender-path `movement_update` +/// The movement core, actor-explicit (stage 4): everything the old sender-path `movement_update` /// did after resolving WHO moved. The trusted gateway path (`gw::gw_movement_update`) delegates here /// — same shape as every `actor.rs` verb, factored out per that file's own rule for still-inlined cores. #[allow(clippy::too_many_arguments)] @@ -1429,7 +1429,7 @@ pub(crate) fn apply_movement_update( if crate::taxi::movement_is_suppressed(ctx, mover.guid) { return Ok(()); } - // #110 ground truth — see MOVEMENT_ENTRIES. Counted in the CORE, not the sender reducer, so + // Ground truth — see MOVEMENT_ENTRIES. Counted in the CORE, not the sender reducer, so // the gateway path's heartbeats land in the same total. { let n = MOVEMENT_ENTRIES.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1; @@ -1521,7 +1521,7 @@ pub(crate) fn apply_movement_update( } } // Rest state (196): not grid-gated (an inn is smaller than a 50yd cell) but THROTTLED to - // ~1Hz per mover (#482): gate on the heartbeat clock crossing a second boundary. At run + // ~1Hz per mover: gate on the heartbeat clock crossing a second boundary. At run // speed that is a check every ~7yd — still finer than any inn — and it removes a per- // heartbeat rest evaluation from the hottest path on the server (measured: the per-move // hook chain was ~58µs at 10k moves/s). @@ -1614,7 +1614,7 @@ pub(crate) fn apply_movement_update( // PER-MOVER MOTION ROW (perf catalog 2.1) — the ONLY movement relay path. Gated on // `plan.relay_motion` — which `plan_movement`'s own tests prove is ALWAYS `true` — never on // `plan.persist_entity` or any raw movement state re-derived here: recreating that coupling is - // exactly the #109 regression this used to guard against with a much larger, rename-brittle scan. + // exactly the regression this used to guard against with a much larger, rename-brittle scan. // // This replaced a 25-probe recipient scan plus one `game_movement_event` INSERT per nearby // player: O(C) writes per heartbeat, so O(C²) per second zone-wide, plus the same again for the @@ -1622,7 +1622,7 @@ pub(crate) fn apply_movement_update( // gateway's AOI tracker already subscribes this exact 5×5 box, so recipient selection was being // computed twice — once here, once by the subscription engine. Only the second one remains. // - // #461: it no longer writes the PUBLIC `game_entity_motion` row inline. SpacetimeDB pays its + // It no longer writes the PUBLIC `game_entity_motion` row inline. SpacetimeDB pays its // subscription sweep per TRANSACTION, so one transaction per movement packet cost 7.8 ms/call at // 500 players (98.5% of it subscription work, 1.5% our wasm). `queue_motion` stages the payload // in the PRIVATE `game_entity_motion_pending` table — no subscribers, therefore no sweep — and @@ -1653,7 +1653,7 @@ pub(crate) fn apply_movement_update( // Targeting // =========================================================================================== -/// The target-write core, actor-explicit (#468 stage 4a) — `gw::gw_set_target` delegates here. +/// The target-write core, actor-explicit (stage 4a) — `gw::gw_set_target` delegates here. pub(crate) fn apply_set_target( ctx: &ReducerContext, mut player: WorldEntity, @@ -1703,7 +1703,7 @@ pub(crate) fn can_inspect( /// fail-open convention as `combat::start_attack`'s friendly-gate, so a dev/test server without an /// imported `FactionTemplate.dbc` never blocks inspect. /// -/// The inspect core, actor-explicit (#479) — same split as [`apply_set_target`]. +/// The inspect core, actor-explicit — same split as [`apply_set_target`]. pub(crate) fn apply_inspect( ctx: &ReducerContext, inspector: crate::WorldEntity, @@ -1904,7 +1904,7 @@ pub(crate) fn ghost_restored_fields(player_flags: u32, unit_bytes_1: u32) -> (bo /// turns the row change into a VALUES packet, which is the ONLY way an observer learns that someone /// else drew or stowed a weapon. Without this the field stays 0 forever and every peer renders every /// player permanently unarmed. Where a stowed weapon hangs is a different field — the per-item -/// `item_template.sheath` byte in the item query. [#101] +/// `item_template.sheath` byte in the item query. pub(crate) fn apply_set_sheathed( ctx: &ReducerContext, mut actor: WorldEntity, @@ -2114,7 +2114,7 @@ pub(crate) fn remove_from_world(ctx: &ReducerContext, owner: Identity) { // its own attack and any attacks targeting it (future PvP). crate::combat::disengage(ctx, entity.guid); - // A live Trade Session dies with the leaver — the partner hears `TradeCanceled` (#120). + // A live Trade Session dies with the leaver — the partner hears `TradeCanceled`. crate::trade::cancel_trade_for(ctx, entity.guid); // Clear the player's corpse on leaving the world (logout/disconnect) so a dead/ghost @@ -2155,7 +2155,7 @@ pub(crate) fn remove_from_world(ctx: &ReducerContext, owner: Identity) { // The per-mover motion row dies with the entity (perf catalog 2.1's named lifecycle gap): a row // left behind never updates again, so it relays nothing, but it would leak one row per character - // that has ever moved and hand a fresh subscriber a stale motion on first sync. #461: the + // that has ever moved and hand a fresh subscriber a stale motion on first sync. The // STAGED payload goes with it, so the next `publish_motion` firing cannot re-create the row we // are deleting here (the tick's own liveness gate is the second net, not the only one). crate::motion::drop_pending(ctx, entity.guid); @@ -2171,7 +2171,7 @@ pub(crate) fn remove_from_world(ctx: &ReducerContext, owner: Identity) { /// `debug_gossip_select` drives the same fire by explicit guid for the harness (the CLI identity /// owns no entity). /// -/// The gossip-notify core, actor-explicit (#479) — same split as [`apply_set_target`], keyed by +/// The gossip-notify core, actor-explicit — same split as [`apply_set_target`], keyed by /// guid (the hook payload is the only thing the body reads off the actor). pub(crate) fn apply_gossip_select( ctx: &ReducerContext, @@ -2278,9 +2278,9 @@ mod tests { /// The WIRING, and the half that matters most: the gate covers the ENTITY row and must never /// reach the per-mover MOTION row. `game_entity_motion` is the only peer-movement relay when - /// AOI is on — gating it would recreate #109 (peers frozen, server perfectly healthy) as a + /// AOI is on — gating it would recreate (peers frozen, server perfectly healthy) as a /// performance optimisation. - // ---- Issue #385: movement_update as plan-then-apply -------------------------------------- + // ---- movement_update as plan-then-apply -------------------------------------- // // `plan_movement` (and the `MovementDelta`/`resolve_fall_damage` pieces it's built from) replace // the reducer's own formatted source as what the "relay is never gated on the persist decision" @@ -2709,10 +2709,10 @@ mod tests { assert_eq!(RESURRECTION_SICKNESS_SPELL, 15007); } - // ---- Issue #59 defect 1: cascade_delete_character ratchets the guid allocator ------------- + // ---- defect 1: cascade_delete_character ratchets the guid allocator ------------- /// `body_of`/`code_of`/`shape_of` are the shared scan primitives in [`crate::test_scan`] - /// (issue #64 — this used to be six near-identical copies, drifted apart). + /// (this used to be six near-identical copies, drifted apart). use crate::test_scan::shape_of; /// Every character-delete path — transfer-driven, CMSG_CHAR_DELETE, `debug_delete_character` — @@ -2733,7 +2733,7 @@ mod tests { /// row against a guid that no longer existed, which the aggro pass then refuses to re-arm /// (it skips anything already attacking) and nothing ever disengages. /// - /// Issue #365: this used to assert the three hand-rolled needles (`melee.attacker_guid().delete`, + /// This used to assert the three hand-rolled needles (`melee.attacker_guid().delete`, /// `melee.by_target().filter`, `threat::clear_for_unit`) that `cascade_delete_character` inlined /// instead of calling the real `combat::disengage` helper — which is exactly why the hand-roll /// was free to drift (it silently omitted `disengage`'s IN_COMBAT clear on freed attackers). Now diff --git a/module/src/xp.rs b/module/src/xp.rs index 1eac4b17d..ac051b093 100644 --- a/module/src/xp.rs +++ b/module/src/xp.rs @@ -364,7 +364,7 @@ pub(crate) fn grant_xp(ctx: &ReducerContext, p: &mut WorldEntity, amount: u32) { // skill-up window `raise_skill`/`gain_weapon_skill`/`gain_defense_skill` climb into. crate::skill::raise_combat_caps(ctx, p.guid, p.level); // Attributes + armor + max health/power for the new level, via the ONE shared writer - // (`stats::apply_level_stats` — also used by login and a GM level-set, #362) so this can never + // (`stats::apply_level_stats` — also used by login and a GM level-set) so this can never // drift from either. `delta` carries the pre-recompute values for the popup math below — // without this the ding loop would otherwise leave STR/AGI/STA/INT/SPI/armor frozen at their // pre-ding values until the next relog, even though combat reads these STORED fields directly @@ -421,7 +421,7 @@ pub(crate) fn grant_xp(ctx: &ReducerContext, p: &mut WorldEntity, amount: u32) { ); } if leveled { - // Sheet AP/damage-range are level-derived (#517) and only ever move via `recompute_sheet`, + // Sheet AP/damage-range are level-derived and only ever move via `recompute_sheet`, // which re-fetches the row by guid — so the ding's level/stat write must be PERSISTED first // (a mid-loop call would see the still-stale pre-ding row). Pull the recomputed row back into // `p` afterward so the caller's own `entities.guid().update(p)` (documented above as the From e699fe2b92dd735ba8534d9c13fcd1daca0ecb74 Mon Sep 17 00:00:00 2001 From: Mathih13 Date: Thu, 3 Sep 2026 21:36:00 +0200 Subject: [PATCH 2/3] fix(module): harden comment tracker scan --- module/src/spell/taxonomy.rs | 8 +- module/src/tripwires.rs | 221 ++++++++++++++++++++++++++++++----- 2 files changed, 193 insertions(+), 36 deletions(-) diff --git a/module/src/spell/taxonomy.rs b/module/src/spell/taxonomy.rs index fc06269f4..fced398b1 100644 --- a/module/src/spell/taxonomy.rs +++ b/module/src/spell/taxonomy.rs @@ -234,8 +234,7 @@ pub(crate) const SPELLMOD_OP_CASTING_TIME: i32 = 10; // Slot 11 of the p0_kind taxonomy. Not read by any engine seam yet (the spell-mod fold reaches the // op through `A_SPELLMOD_*`'s own `p0`), but the NUMBER is a stored data contract: the importer // writes p0_kind values into `game_spell_effect`, so deleting the name would leave 11 undocumented -// and free for a future kind to collide with. Named here, deliberately unused. -#[allow(dead_code)] +// and free for a future kind to collide with. pub(crate) const P_SPELLMOD_OP: u8 = 11; // p0 is a SpellModOp (A_SPELLMOD_*: 0=damage, 7=crit, 10=cast time, 14=cost); p1 carries the 32-bit affected-spell family mask /// The effect's `amount` is a PERCENT of the caster's MAX power, not an absolute figure (Mage Evocation's /// A_PERIODIC_ENERGIZE restores 15% of max mana per tick). `aura_apply` reads this tag and converts the @@ -451,8 +450,8 @@ pub(crate) const ALL_AURA_KINDS: &[u8] = &[ // drifted the way the two lists above did — so it does not need a canonical, loop-tested slice of its // own. A subset of it has no OTHER production call site yet though (only a test-only reference, same as // every `E_*`/`A_*` kind before `ALL_INSTANT_KINDS`/`ALL_AURA_KINDS` existed), so it still needs SOME -// scaffold to keep `dead_code` from tripping on the non-test `lib` build — this is that scaffold, sized -// to just the residue (down from `_TAXONOMY`'s ~100 entries to these 16). +// aggregate to keep `dead_code` from tripping on the non-test `lib` build. It holds just the residue, +// down from `_TAXONOMY`'s ~100 entries to these 18. #[allow(dead_code)] const _RESERVED_NON_KIND_TAXONOMY: &[u8] = &[ P_NONE, @@ -465,6 +464,7 @@ const _RESERVED_NON_KIND_TAXONOMY: &[u8] = &[ P_ITEM_ENTRY, P_ENTRY, P_ENCHANT_ID, + P_SPELLMOD_OP, P_DISPLAY_ID, P_RAW, T_SCRIPTED, diff --git a/module/src/tripwires.rs b/module/src/tripwires.rs index 66d8788b7..ddee09eef 100644 --- a/module/src/tripwires.rs +++ b/module/src/tripwires.rs @@ -1291,45 +1291,181 @@ pub(crate) mod grid_cell_tripwire { /// ENFORCEMENT tripwire: a comment must stay understandable without tracker history, so no comment /// under `module/src` may carry an issue reference (a hash sign followed by 2..=4 digits). The -/// allow-list below is a RATCHET of MAXIMUMS: cleaning a file up lowers its number, and a file that -/// reaches zero drops off the list entirely. +/// allow-list below records exact counts. Cleanup updates the count, and a file at zero leaves the +/// list entirely. #[cfg(test)] pub(crate) mod issue_reference_tripwire { /// `(path under `module/src`, tracker references still tolerated there)`. const RATCHET: &[(&str, usize)] = &[("spell/cast/targeting.rs", 6)]; - /// Lines whose comment carries a tracker reference: a hash sign opening a 2..=4 digit token - /// after a space or an opening bracket. A build, opcode, patch or revision number is not one. + /// Returns one line number for each tracker reference in a comment. A reference is a hash sign + /// opening a 2..=4 digit token; a build, opcode, patch or revision number is not one. fn tracker_refs(content: &str) -> Vec { let bytes = content.as_bytes(); let mut out = Vec::new(); - for (i, _) in content.match_indices('#') { - let digits = bytes[i + 1..] - .iter() - .take_while(|c| c.is_ascii_digit()) - .count(); - if !(2..=4).contains(&digits) || i == 0 || !matches!(bytes[i - 1], b' ' | b'(' | b'[') { - continue; - } - if !crate::test_scan::on_comment_line(content, i) { + let mut i = 0; + + while i < bytes.len() { + if let Some(end) = raw_string_end(bytes, i) { + i = end; continue; } - let head = content[..i - 1].trim_end(); - let word = &head[head.rfind(char::is_whitespace).map_or(0, |j| j + 1)..]; - if matches!(word, "build" | "opcode" | "patch" | "rev") { - continue; + + match bytes[i] { + b'/' if bytes.get(i + 1) == Some(&b'/') => { + let end = bytes[i + 2..] + .iter() + .position(|byte| *byte == b'\n') + .map_or(bytes.len(), |offset| i + 2 + offset); + comment_refs(content, i + 2, end, &mut out); + i = end; + } + b'/' if bytes.get(i + 1) == Some(&b'*') => { + i = block_comment_refs(content, i + 2, &mut out); + } + b'"' => i = quoted_end(bytes, i, b'"'), + b'\'' => i = char_literal_end(bytes, i).unwrap_or(i + 1), + _ => i += 1, } - out.push(crate::test_scan::line_of(content, i)); } out } + fn comment_refs(content: &str, start: usize, end: usize, out: &mut Vec) { + for (offset, byte) in content.as_bytes()[start..end].iter().enumerate() { + let index = start + offset; + if *byte == b'#' && is_tracker_ref(content, index) { + out.push(crate::test_scan::line_of(content, index)); + } + } + } + + fn block_comment_refs(content: &str, mut i: usize, out: &mut Vec) -> usize { + let bytes = content.as_bytes(); + let mut depth = 1; + while i < bytes.len() && depth > 0 { + if bytes.get(i..i + 2) == Some(b"/*") { + depth += 1; + i += 2; + } else if bytes.get(i..i + 2) == Some(b"*/") { + depth -= 1; + i += 2; + } else { + if bytes[i] == b'#' && is_tracker_ref(content, i) { + out.push(crate::test_scan::line_of(content, i)); + } + i += 1; + } + } + i + } + + fn raw_string_end(bytes: &[u8], start: usize) -> Option { + let mut quote = start; + if bytes.get(quote) == Some(&b'b') { + quote += 1; + } + if bytes.get(quote) != Some(&b'r') { + return None; + } + quote += 1; + let hashes = bytes[quote..] + .iter() + .take_while(|byte| **byte == b'#') + .count(); + quote += hashes; + if bytes.get(quote) != Some(&b'"') { + return None; + } + + let mut i = quote + 1; + while i < bytes.len() { + if bytes[i] == b'"' + && bytes.get(i + 1..i + 1 + hashes) == Some(&bytes[quote - hashes..quote]) + { + return Some(i + 1 + hashes); + } + i += 1; + } + Some(bytes.len()) + } + + fn quoted_end(bytes: &[u8], mut i: usize, quote: u8) -> usize { + i += 1; + while i < bytes.len() { + if bytes[i] == b'\\' { + i += 2; + } else if bytes[i] == quote { + return i + 1; + } else { + i += 1; + } + } + bytes.len() + } + + fn char_literal_end(bytes: &[u8], start: usize) -> Option { + if bytes + .get(start + 1) + .is_some_and(|byte| byte.is_ascii_alphabetic() || *byte == b'_') + && bytes.get(start + 2) != Some(&b'\'') + { + return None; + } + + let end = (start + 9).min(bytes.len()); + let mut i = start + 1; + while i < end { + if bytes[i] == b'\\' { + i += 2; + } else if bytes[i] == b'\'' { + return Some(i + 1); + } else { + i += 1; + } + } + None + } + + fn is_tracker_ref(content: &str, index: usize) -> bool { + let bytes = content.as_bytes(); + let digits = bytes[index + 1..] + .iter() + .take_while(|byte| byte.is_ascii_digit()) + .count(); + if !(2..=4).contains(&digits) + || bytes + .get(index + 1 + digits) + .is_some_and(u8::is_ascii_digit) + { + return false; + } + + let before = content[..index].trim_end_matches(|character: char| { + character.is_ascii_whitespace() || character == '(' || character == '[' + }); + let word = before + .rsplit(|character: char| !character.is_ascii_alphabetic()) + .next() + .unwrap_or_default(); + !matches!(word, "build" | "opcode" | "patch" | "rev" | "revision") + } + + fn ratchet_mismatch(path: &str, found: usize, budget: usize) -> Option { + (found != budget).then(|| format!("{path}: {found} refs (ratchet {budget})")) + } + + fn ratchet_file_is_missing(path: &str, seen: &[String]) -> bool { + !seen.iter().any(|seen_path| seen_path == path) + } + #[test] fn no_comment_carries_a_tracker_issue_reference() { let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); let mut files = Vec::new(); super::character_owned_tripwire::collect_rs_files(&root, &mut files); let mut violations = Vec::new(); + let mut seen = Vec::new(); for file in files { let rel = file .strip_prefix(&root) @@ -1338,17 +1474,23 @@ pub(crate) mod issue_reference_tripwire { .replace('\\', "/"); let content = std::fs::read_to_string(&file).expect("readable module source"); let lines = tracker_refs(&content); - let budget = RATCHET - .iter() - .find(|(p, _)| *p == rel) - .map_or(0, |(_, n)| *n); - if lines.len() > budget { - let found = lines.len(); + seen.push(rel.clone()); + if let Some((_, budget)) = RATCHET.iter().find(|(path, _)| *path == rel) { + if let Some(mismatch) = ratchet_mismatch(&rel, lines.len(), *budget) { + violations.push(format!("{mismatch}, lines {lines:?}")); + } + } else if !lines.is_empty() { violations.push(format!( - "{rel}: {found} refs (budget {budget}), lines {lines:?}" + "{rel}: {} refs (ratchet 0), lines {lines:?}", + lines.len() )); } } + for (path, _) in RATCHET { + if ratchet_file_is_missing(path, &seen) { + violations.push(format!("{path}: ratchet file is missing")); + } + } assert!( violations.is_empty(), "tracker issue references found in module/src comments:\n {}\n\n\ @@ -1358,14 +1500,29 @@ pub(crate) mod issue_reference_tripwire { ); } - /// The scan catches a reference and leaves a build number, an opcode id and live code alone. + /// The scan covers line, trailing and nested block comments, while leaving literals alone. #[test] - fn the_scan_separates_a_tracker_reference_from_an_ordinary_number() { + fn the_scan_covers_comments_without_reading_literals() { let h = '#'; - assert_eq!(tracker_refs(&format!("// a note ({h}119)\n")), vec![1]); - assert_eq!(tracker_refs(&format!("/// see {h}42 for why\n")), vec![1]); - assert!(tracker_refs(&format!("// build {h}5875 ships it\n")).is_empty()); - assert!(tracker_refs(&format!("// opcode {h}117 is CMSG\n")).is_empty()); - assert!(tracker_refs(&format!("let s = \"({h}119)\";\n")).is_empty()); + let source = format!( + "// full {h}119\nlet x = 0; // pre-{h}22\n/* outer {h}23 /* nested {h}24 */ tail {h}25 */\nfn f<'a /* issue {h}22 */>() {{}}\nlet normal = \"{h}26\";\nlet escaped = \"\\\\\"{h}27\";\nlet raw = r###\"{h}28\"###;\nlet byte_raw = br#\"{h}29\"#;\nlet character = '{h}';\n// build {h}5875 and opcode {h}117\n// build ({h}5875) and opcode [{h}117]\n" + ); + assert_eq!(tracker_refs(&source), vec![1, 2, 3, 3, 3, 4]); + } + + #[test] + fn the_ratchet_rejects_stale_counts() { + assert!(ratchet_mismatch("fixture.rs", 1, 1).is_none()); + assert!(ratchet_mismatch("fixture.rs", 0, 1).is_some()); + assert!(ratchet_mismatch("fixture.rs", 2, 1).is_some()); + } + + #[test] + fn the_ratchet_rejects_a_missing_allow_listed_file() { + assert!(!ratchet_file_is_missing( + "fixture.rs", + &["fixture.rs".to_owned()] + )); + assert!(ratchet_file_is_missing("fixture.rs", &[])); } } From 134b4d6ee1649c0f2cce223144aaaa8187f2d946 Mon Sep 17 00:00:00 2001 From: Mathih13 Date: Thu, 3 Sep 2026 21:45:47 +0200 Subject: [PATCH 3/3] fix(module): pin source hygiene allowances --- module/src/creatures/eventai/edges.rs | 2 - module/src/tripwires.rs | 64 ++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/module/src/creatures/eventai/edges.rs b/module/src/creatures/eventai/edges.rs index 472d1e05b..6153a2f71 100644 --- a/module/src/creatures/eventai/edges.rs +++ b/module/src/creatures/eventai/edges.rs @@ -263,7 +263,6 @@ pub(crate) fn eventai_on_reached_home(ctx: &ReducerContext, creature_guid: u64) ); } -#[allow(dead_code, reason = "typed producer seam for EventAI relay actions")] pub(crate) fn eventai_on_receive_ai_event( ctx: &ReducerContext, creature_guid: u64, @@ -350,7 +349,6 @@ pub(crate) fn send_relay_ai_event( Ok(()) } -#[allow(dead_code, reason = "typed producer seam for pathfinding authority")] pub(crate) fn eventai_on_target_not_reachable(ctx: &ReducerContext, creature_guid: u64) { let current_target_guid = ctx .db diff --git a/module/src/tripwires.rs b/module/src/tripwires.rs index ddee09eef..dc94dea94 100644 --- a/module/src/tripwires.rs +++ b/module/src/tripwires.rs @@ -1448,7 +1448,9 @@ pub(crate) mod issue_reference_tripwire { .rsplit(|character: char| !character.is_ascii_alphabetic()) .next() .unwrap_or_default(); - !matches!(word, "build" | "opcode" | "patch" | "rev" | "revision") + !["build", "opcode", "patch", "rev", "revision"] + .iter() + .any(|exception| word.eq_ignore_ascii_case(exception)) } fn ratchet_mismatch(path: &str, found: usize, budget: usize) -> Option { @@ -1505,7 +1507,7 @@ pub(crate) mod issue_reference_tripwire { fn the_scan_covers_comments_without_reading_literals() { let h = '#'; let source = format!( - "// full {h}119\nlet x = 0; // pre-{h}22\n/* outer {h}23 /* nested {h}24 */ tail {h}25 */\nfn f<'a /* issue {h}22 */>() {{}}\nlet normal = \"{h}26\";\nlet escaped = \"\\\\\"{h}27\";\nlet raw = r###\"{h}28\"###;\nlet byte_raw = br#\"{h}29\"#;\nlet character = '{h}';\n// build {h}5875 and opcode {h}117\n// build ({h}5875) and opcode [{h}117]\n" + "// full {h}119\nlet x = 0; // pre-{h}22\n/* outer {h}23 /* nested {h}24 */ tail {h}25 */\nfn f<'a /* issue {h}22 */>() {{}}\nlet normal = \"{h}26\";\nlet escaped = \"\\\\\"{h}27\";\nlet raw = r###\"{h}28\"###;\nlet byte_raw = br#\"{h}29\"#;\nlet character = '{h}';\n// build {h}5875 and opcode {h}117\n// build ({h}5875) and opcode [{h}117]\n// BUILD {h}5875, OpCoDe ({h}117), PATCH [{h}123], ReV {h}456, ReViSiOn {h}789\n" ); assert_eq!(tracker_refs(&source), vec![1, 2, 3, 3, 3, 4]); } @@ -1526,3 +1528,61 @@ pub(crate) mod issue_reference_tripwire { assert!(ratchet_file_is_missing("fixture.rs", &[])); } } + +/// ENFORCEMENT tripwire: five unconditional `dead_code` allowances document the remaining +/// deliberate residues. A conditional `cfg_attr` allowance does not count here. +#[cfg(test)] +pub(crate) mod dead_code_allowance_tripwire { + const EXPECTED_UNCONDITIONAL_ALLOWANCES: usize = 5; + + fn unconditional_dead_code_allows(content: &str) -> Vec { + let mut lines = Vec::new(); + for (start, _) in content.match_indices("#[allow(") { + let line_start = content[..start].rfind('\n').map_or(0, |index| index + 1); + if !content[line_start..start].trim().is_empty() { + continue; + } + let Some(end) = content[start..].find(")]") else { + continue; + }; + if content[start..start + end].contains("dead_code") { + lines.push(crate::test_scan::line_of(content, start)); + } + } + lines + } + + #[test] + fn module_has_exactly_five_unconditional_dead_code_allowances() { + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut files = Vec::new(); + super::character_owned_tripwire::collect_rs_files(&root, &mut files); + let mut found = Vec::new(); + for file in files { + let rel = file + .strip_prefix(&root) + .expect("scanned under module/src") + .to_string_lossy() + .replace('\\', "/"); + let content = std::fs::read_to_string(&file).expect("readable module source"); + found.extend( + unconditional_dead_code_allows(&content) + .into_iter() + .map(|line| format!("{rel}:{line}")), + ); + } + assert_eq!( + found.len(), + EXPECTED_UNCONDITIONAL_ALLOWANCES, + "unconditional dead_code allowances changed:\n {}", + found.join("\n ") + ); + } + + #[test] + fn the_scan_excludes_conditional_allowances() { + let source = + "#[allow(dead_code, reason = \"residue\")]\n#[cfg_attr(test, allow(dead_code))]\n"; + assert_eq!(unconditional_dead_code_allows(source), vec![1]); + } +}