From aedd8f218ae6629ba30bf1edbd2022d5854aae0b Mon Sep 17 00:00:00 2001 From: Mathih13 Date: Wed, 7 Oct 2026 23:54:00 +0200 Subject: [PATCH 1/5] fix(package-api): skip a held name when creating a Package-owned Account Co-Authored-By: Claude Opus 5.5 (1M context) --- module/src/package_account.rs | 18 +++++++++++------- module/tests/package_account.rs | 23 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/module/src/package_account.rs b/module/src/package_account.rs index cf6fc173d..9116899cd 100644 --- a/module/src/package_account.rs +++ b/module/src/package_account.rs @@ -17,8 +17,8 @@ pub struct PackageAccount { /// Create a Character with no Session on an Account `package_name` owns, and return its guid. The /// Character meets the same name, race and class Refusals as a client-created one, and those -/// Refusals write nothing. The Character goes on the first owned Account with room; when every one is full, -/// a new Account without credentials is created and recorded as owned by `package_name`. +/// Refusals write nothing. The Character goes on the first owned Account with room. When every one +/// is full, a new Account without credentials is created and recorded as owned by `package_name`. #[cfg_attr(not(has_packages), allow(dead_code))] // package-only caller — see `build.rs` pub(crate) fn create_package_character( ctx: &ReducerContext, @@ -53,13 +53,17 @@ fn owned_account_with_room(ctx: &ReducerContext, package_name: &str) -> Result Date: Wed, 7 Oct 2026 23:54:00 +0200 Subject: [PATCH 2/5] test(module): fail when Core source names an official Package Co-Authored-By: Claude Opus 5.5 (1M context) --- module/src/tripwires.rs | 90 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/module/src/tripwires.rs b/module/src/tripwires.rs index 26d59cdba..4f6eea250 100644 --- a/module/src/tripwires.rs +++ b/module/src/tripwires.rs @@ -3,7 +3,7 @@ //! that doc comment stays true. `#[cfg(test)] mod tripwires;` is `lib.rs`'s only mention of this //! file. //! -//! Nine tripwires, in file order: +//! Ten tripwires, in file order: //! - [`character_owned_tripwire`] — every table with a Character-capable guid field has a //! `character_owned` sweep marker or an explicit exclusion. //! - [`build_scan_strip_tripwire`] — a commented-out marker invocation never registers. @@ -15,6 +15,7 @@ //! - [`grid_cell_tripwire`] — indexed cell fields are derived from their grid coordinates. //! - [`issue_reference_tripwire`] — no comment carries a tracker issue reference. //! - [`dead_code_allowance_tripwire`] — dead-code allowances stay confined to declared boundaries. +//! - [`package_name_tripwire`] — Core source names no official Package. //! //! `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 @@ -2226,3 +2227,90 @@ pub(crate) mod dead_code_allowance_tripwire { assert_eq!(unconditional_dead_code_allows(source), vec![1]); } } + +/// ENFORCEMENT tripwire: Core source names no official Package, in code, comments or the +/// generated Gateway bindings. Core must build and read the same with no Package installed. +#[cfg(test)] +pub(crate) mod package_name_tripwire { + use std::path::{Path, PathBuf}; + + /// The official Packages (LyraCoreProject/packages) that Core must not name. + const OFFICIAL_PACKAGES: &[&str] = &["playerbots"]; + + const CORE_TREES: &[&str] = &["module", "gateway", "crates", "importer"]; + + /// Test-only files may name a Package, and this file holds the list. + fn is_exempt(rel: &str) -> bool { + rel == "module/src/tripwires.rs" + || rel.ends_with("_tests.rs") + || rel.ends_with("/tests.rs") + || rel.split('/').any(|part| part == "tests") + } + + fn named_packages(content: &str) -> Vec<(usize, &'static str)> { + let content = content.to_ascii_lowercase(); + let mut found = Vec::new(); + for (index, line) in content.lines().enumerate() { + for name in OFFICIAL_PACKAGES { + if line.contains(name) { + found.push((index + 1, *name)); + } + } + } + found + } + + fn collect_files(dir: &Path, out: &mut Vec) { + let entries = + std::fs::read_dir(dir).unwrap_or_else(|e| panic!("cannot read {}: {e}", dir.display())); + for entry in entries { + let path = entry.expect("readable dir entry").path(); + if path.is_dir() { + collect_files(&path, out); + } else { + out.push(path); + } + } + } + + #[test] + fn core_source_names_no_official_package() { + let root = crate::test_scan::repo_root(); + let mut files = Vec::new(); + for tree in CORE_TREES { + collect_files(&root.join(tree), &mut files); + } + let mut found = Vec::new(); + for file in files { + let rel = file + .strip_prefix(&root) + .expect("walked from the repo root") + .to_string_lossy() + .replace('\\', "/"); + if is_exempt(&rel) { + continue; + } + let bytes = std::fs::read(&file).expect("readable Core file"); + for (line, name) in named_packages(&String::from_utf8_lossy(&bytes)) { + found.push(format!("{rel}:{line} names {name}")); + } + } + assert!( + found.is_empty(), + "Core source names an official Package:\n {}\n\nName the capability instead.", + found.join("\n ") + ); + } + + #[test] + fn the_scan_reads_comments_and_bindings_but_not_tests() { + let source = "let x = 1;\n/// a Playerbots brain\nlet t = \"pkg_playerbots_bot\";\n"; + assert_eq!( + named_packages(source), + vec![(2, "playerbots"), (3, "playerbots")] + ); + assert!(!is_exempt("gateway/src/stdb/bindings/mod.rs")); + assert!(is_exempt("gateway/src/world/party_tests.rs")); + assert!(is_exempt("module/tests/package_account.rs")); + } +} From 1381b21de7d8a9495d0368fa6b7baf82bb4d1b24 Mon Sep 17 00:00:00 2001 From: Mathih13 Date: Wed, 7 Oct 2026 23:54:00 +0200 Subject: [PATCH 3/5] ci: build the Module with the official Packages from packages main Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/rust.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 54d93d57b..fc1fe1320 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -52,6 +52,22 @@ jobs: # `lyracore publish` builds this feature set. A default-feature check would omit published reducers. - run: cargo check --target wasm32-unknown-unknown -p lyracore-module --features=debug_reducers + # A Core change that breaks an official Package fails here, before the Package's next publish. + official-packages: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: LyraCoreProject/packages + ref: main + path: official-packages + persist-credentials: false + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + - name: Install the official Packages + run: for package in official-packages/*/; do ln -s "$PWD/$package" packages/; done + - run: cargo check --target wasm32-unknown-unknown -p lyracore-module --features=debug_reducers + deploy-scripts: runs-on: ubuntu-latest steps: From 9480304575afd4703a5d2060664495fae9af6709 Mon Sep 17 00:00:00 2001 From: Mathih13 Date: Wed, 7 Oct 2026 23:54:00 +0200 Subject: [PATCH 4/5] docs: describe Package capabilities without naming playerbots Co-Authored-By: Claude Opus 5.5 (1M context) --- gateway/src/main.rs | 2 +- gateway/src/world/party.rs | 8 ++++---- gateway/src/world/presence.rs | 2 +- gateway/src/world/transfer.rs | 2 +- module/src/actor.rs | 6 +++--- module/src/config.rs | 2 +- module/src/creatures/tick/mod.rs | 2 +- module/src/debug/audit.rs | 2 +- module/src/debug/mod.rs | 4 ++-- module/src/group.rs | 27 +++++++++++++-------------- module/src/gw.rs | 2 +- module/src/hooks.rs | 4 ++-- module/src/instance.rs | 2 +- module/src/loot/tag.rs | 4 ++-- module/src/meeting_stone/mod.rs | 2 +- module/src/package_config.rs | 4 ++-- module/src/seed/fixtures.rs | 2 +- module/src/trainer.rs | 4 ++-- module/src/transfer/mod.rs | 2 +- module/src/world.rs | 2 +- 20 files changed, 42 insertions(+), 43 deletions(-) diff --git a/gateway/src/main.rs b/gateway/src/main.rs index 643966b99..a63dbda54 100644 --- a/gateway/src/main.rs +++ b/gateway/src/main.rs @@ -112,7 +112,7 @@ async fn run() -> Result<()> { // Here because this is the first point the realm row is readable. warn_on_unreachable_realm_address(&coordinator, &cfg); - // Bot-initiated (serendipity) invites: a playerbot's goal tick has no client and no + // Bot-initiated (serendipity) invites: a session-less Character's invite has no client and no // player connection to ride, so it is picked up here — on the coordinator, independent of any // session — rather than from a per-player relay. coordinator.spawn_bot_invite_relay(); diff --git a/gateway/src/world/party.rs b/gateway/src/world/party.rs index ef94cecb8..8a5dad9d6 100644 --- a/gateway/src/world/party.rs +++ b/gateway/src/world/party.rs @@ -1000,8 +1000,8 @@ pub(crate) fn run( // // Each gate is the module's own read, unioned across the shards — EXISTS is a `game_character` // row ([`presence::of`]), ONLINE is a `game_world_entity` row ([`presence::live_anywhere`]). - // Reading the session flag for the second would silently refuse every playerbot; see - // [`presence::live_anywhere`]. + // Reading the session flag for the second would silently refuse every session-less Character; + // see [`presence::live_anywhere`]. if let Op::Invite(target) = op { if let Some(refusal) = invite_gate(store, target)? { return Ok(refusal.into()); @@ -1029,7 +1029,7 @@ pub(crate) fn run( { return Ok(PartyOutcome::Refused(refusal)); } - // Nobody is at the keyboard of a playerbot, so nobody answers its dialog. Done + // Nobody is at the keyboard of a session-less Character, so nobody answers its dialog. Done // BEFORE the mirror push, so the ONE push that follows already carries the bot as a member — // which is what the shard's own party reads (kill-XP split, `/p`, follow-the-leader) need. if let Op::Invite(target) = op { @@ -1175,7 +1175,7 @@ pub(crate) fn run_bot_invite_intent( } } -/// Run a SERVER-DRIVEN invite with no client behind it — a playerbot's serendipity pick, closing +/// Run a SERVER-DRIVEN invite with no client behind it — a session-less Character's invite, closing /// the gap the group slice opened: the module used to write this shard's LOCAL /// `game_group`/`game_group_member` rows directly, which the next `sync_group_mirror` push wiped /// because realm-core had never heard of them. diff --git a/gateway/src/world/presence.rs b/gateway/src/world/presence.rs index 67754fa4a..cfe95a7ef 100644 --- a/gateway/src/world/presence.rs +++ b/gateway/src/world/presence.rs @@ -357,7 +357,7 @@ pub(crate) fn character_anywhere( } /// Does `guid` have a live entity on any connected Shard — `game_world_entity`, unioned across the -/// boundary. NOT `game_character.online`: a session-less playerbot is inserted straight into +/// boundary. NOT `game_character.online`: a session-less Character is inserted straight into /// `game_world_entity` and never runs `player_login`, so its session flag stays false for its /// whole life. [`of`] exposes both, as `Whereabouts::InWorld` and `session_online`; this is the /// `in_world`-only shortcut a caller that does not need the rest of [`RealmPresence`] keeps using. diff --git a/gateway/src/world/transfer.rs b/gateway/src/world/transfer.rs index 0b5b2b871..080bc032b 100644 --- a/gateway/src/world/transfer.rs +++ b/gateway/src/world/transfer.rs @@ -479,7 +479,7 @@ fn evict_finished_instance(source: &dyn WorldStore, transfer_id: u64, instance_i } } -/// Drive one Shard crossing for a character with **no session** — a playerbot following its party +/// Drive one Shard crossing for a character with **no session** — a bot following its party /// through a portal, and the same path again on the way out. /// /// A player's crossing is driven inside its own loading screen: the client acks diff --git a/module/src/actor.rs b/module/src/actor.rs index f1145e579..cfaa60cc9 100644 --- a/module/src/actor.rs +++ b/module/src/actor.rs @@ -1,6 +1,6 @@ //! The ACTOR VERB API: one documented surface over every explicit-guid action -//! core, so anything that acts ON BEHALF OF a unit — the `debug_*` harness reducers, the -//! playerbots brains, and the future Tier-2 Lua host API — consumes the SAME verbs the +//! core, so anything that acts ON BEHALF OF a unit — the `debug_*` harness reducers, Package +//! bots, and the future Tier-2 Lua host API — consumes the SAME verbs the //! player reducers do, with identical gates. //! //! Existing verbs retain their `Result<(), String>` contract. Typed requests expose accepted @@ -165,7 +165,7 @@ package_only! { pub(crate) use crate::items::request_profile_item as reconcile_profile_item; pub(crate) use crate::items::apply_equip_profile_upgrade as equip_profile_upgrade; pub(crate) use crate::items::request_take_loot as request_take_loot; - // `loot_money` also feeds playerbots' drink-at-rest behavior (work-item 154). + // A Package bot also loots money through this verb. pub(crate) use crate::loot::apply_loot_money as loot_money; } pub(crate) use crate::creatures::apply_item_target_spell as cast_item_target; diff --git a/module/src/config.rs b/module/src/config.rs index 45ce65f4a..bb61e7d93 100644 --- a/module/src/config.rs +++ b/module/src/config.rs @@ -76,7 +76,7 @@ pub struct ServerConfig { // path: the gateway derives it from the shard map and asserts it at startup. #[default(true)] pub hosts_instances: bool, - // END-APPENDED: park every playerbot where it was spawned — the GOAL brain and the COMBAT brain + // END-APPENDED: park every Package bot where it was spawned — the GOAL and COMBAT brains // return immediately, so a crowd neither picks up quests nor grinds its way out of the zone. The // WANDER pass deliberately keeps running (6 yd hops around home), because a launch-day crowd // milling in a plaza is the movement load worth measuring. The load-test lever: bots that quest diff --git a/module/src/creatures/tick/mod.rs b/module/src/creatures/tick/mod.rs index cbd458000..093a8cf70 100644 --- a/module/src/creatures/tick/mod.rs +++ b/module/src/creatures/tick/mod.rs @@ -975,7 +975,7 @@ mod relay_tripwire { /// table still exists and `gc.rs` sweeps whatever is left.) /// /// 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 + /// a Package file), 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` /// already walks `module/src` plus every installed `packages/*/src`, so a new file (or a moved diff --git a/module/src/debug/audit.rs b/module/src/debug/audit.rs index e0817e505..3eac8fa29 100644 --- a/module/src/debug/audit.rs +++ b/module/src/debug/audit.rs @@ -19,7 +19,7 @@ use crate::{ pub fn debug_audit_class_kits(ctx: &ReducerContext) { use crate::game_trainer_spell; use crate::spell::{A_FLAG, E_SCRIPTED}; - // (class label, trainer creature entry) — the stable CLASS_TRAINERS map (playerbots mod.rs). + // (class label, trainer creature entry): the Goldshire class trainers. const TRAINERS: &[(&str, u32)] = &[ ("Warrior", 913), ("Paladin", 927), diff --git a/module/src/debug/mod.rs b/module/src/debug/mod.rs index a3805487b..ccb8a1174 100644 --- a/module/src/debug/mod.rs +++ b/module/src/debug/mod.rs @@ -103,7 +103,7 @@ pub fn debug_teleport( } /// Record a Transfer Intent for `character_guid`: the session-less Shard crossing a Package's -/// playerbot decides for itself, driven by hand. +/// bot decides for itself, driven by hand. /// /// The lever exists because the acceptance test for that crossing must NOT need a Package installed. /// It calls the same `transfer::emit_bot_transfer_intent` the Package calls, so what it exercises is @@ -171,7 +171,7 @@ pub fn debug_apply_damage( target_guid: u64, amount: u32, // The attributed attacker (0 = anonymous): threads through to `on_damage_taken` so - // attacker-reactive systems (the playerbots defend hook, Retaliation, the proc pass) see a real + // attacker-reactive systems (a Package defend hook, Retaliation, the proc pass) see a real // source. attacker_guid: u64, ) -> Result<(), String> { diff --git a/module/src/group.rs b/module/src/group.rs index 17d9ce45e..0b2368bce 100644 --- a/module/src/group.rs +++ b/module/src/group.rs @@ -19,8 +19,8 @@ //! 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.** 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 +//! **Server-driven invites.** A session-less Character's invite has no client and no +//! `ctx.sender()` to resolve — it is a decision a Package makes inside the Module. 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 — //! picked up by `gateway/src/world/party.rs`'s `run_bot_invite`. The same row carries a bot's @@ -271,11 +271,10 @@ crate::character_owned!(not_transported, fn sweep_transfer_game_group_invite()); /// 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 -/// realm-core had never heard of; the next `sync_group_mirror` push then wiped the party the mirror -/// did not recognise. +/// only the gateway can reach it — the module never has. Before this table existed, a Package's +/// server-driven invite called [`invite_core`] directly, writing this shard's LOCAL +/// `game_group`/`game_group_member` rows, which realm-core had never heard of; the next +/// `sync_group_mirror` push then wiped the party the mirror did not recognise. /// /// The fix splits the decision from the execution, the same way `world::party` already splits a /// player's own invite: the module picks the fellow quester (it already owns the spatial + quest @@ -317,7 +316,7 @@ pub struct BotInviteIntent { /// player's own CMSG_GROUP_INVITE is a pure gateway-side resolve-then-call with no module-side /// pre-check either. /// -/// Its ONLY caller is the playerbots drop-in (see the `package_only!` macro in `actor.rs`): a +/// Its ONLY callers are Packages (see the `package_only!` macro in `actor.rs`): a /// build with no REAL package installed — the common case, since only the inert reference Package, /// `packages/example/`, ships by default — has no caller for this, which is a designed state, not /// dead code. @@ -1269,8 +1268,8 @@ fn push_list_to_all(ctx: &ReducerContext, group_id: u64) { /// `gw::gw_group_invite`. /// /// The identity-free invite core (the `accept_invite_for` pattern): shared by `gw::gw_group_invite` -/// and any server-driven inviter — a playerbot's serendipity invite (276) calls this with -/// the bot's guid. Same gates in the same order for every caller. +/// and any server-driven inviter, which passes the bot's guid. Same gates in the same order for +/// every caller. pub(crate) fn invite_core( ctx: &ReducerContext, inviter_guid: u64, @@ -1304,7 +1303,7 @@ fn invite_core_on( if ctx.db.game_character().guid().find(target_guid).is_none() { return Err(GroupRefusal::NoSuchPlayer.into()); } - // Vanilla requires the target online; a session-less playerbot's live entity counts (its + // Vanilla requires the target online; a session-less Character's live entity counts (its // auto-accept rides the hook below, not a client). if ctx .db @@ -1355,10 +1354,10 @@ fn invite_core_on( String::new(), ); // The hook is PLANE-LOCAL, and this is what that costs: package tables live on the world - // shards, so on a TRUE multi-database realm-core `pkg_playerbots_bot` is empty and the playerbots + // shards, so on a TRUE multi-database realm-core a Package's bot table is empty and its // auto-accept handler returns immediately — a player's invite to a bot was created correctly and // then nobody answered it (observed live 2026-07-26). Fired here regardless: a single-database - // gateway's realm-core IS the world shard, so `pkg_playerbots_bot` is populated and the hook + // gateway's realm-core IS the world shard, so the bot table is populated and the hook // still answers in this transaction there — including every bot-to-bot serendipity invite // which since that fix runs through `realm_group_op`/Plane::RealmCore like every // other invite, never through `invite_core`/Plane::Shard directly. On a real multi-database @@ -1379,7 +1378,7 @@ fn invite_core_on( /// via `gw::gw_accept_group_invite`. /// /// The identity-free accept core: shared by `gw::gw_accept_group_invite` and any server-driven -/// acceptor (a playerbot's auto-accept hook calls this with the bot's guid). +/// acceptor (a Package auto-accept hook calls this with the bot's guid). pub(crate) fn accept_invite_for(ctx: &ReducerContext, acceptor_guid: u64) -> Result<(), String> { let acceptor = crate::helpers::character_by_guid(ctx, acceptor_guid); if acceptor.as_ref().is_some_and(|character| !character.online) { diff --git a/module/src/gw.rs b/module/src/gw.rs index be142c3f3..1c12efc90 100644 --- a/module/src/gw.rs +++ b/module/src/gw.rs @@ -442,7 +442,7 @@ pub fn gw_turn_in_quest( let actor_guid = crate::account_ownership::require_actor(ctx, request_actor)?; actor(ctx, actor_guid)?; crate::actor::turn_in_quest(ctx, actor_guid, giver_guid, quest_entry, reward_index)?; - // Only the World Session files a Reward Letter: no Gateway drives a playerbot's mail, so the + // Only the World Session files a Reward Letter: no Gateway drives a bot's mail, so the // Package turn-in path would leave the letter held. crate::mail_reward::file_reward_letter(ctx, actor_guid, giver_guid, quest_entry) } diff --git a/module/src/hooks.rs b/module/src/hooks.rs index 9b8903428..ce60baf67 100644 --- a/module/src/hooks.rs +++ b/module/src/hooks.rs @@ -68,8 +68,8 @@ pub struct CreatureSpawnPayload { } /// A group invite was just recorded for `target_guid`. Fired AFTER the invite row + notification -/// event are written — a handler may accept immediately via `group::accept_invite_for` (the -/// playerbots auto-accept path). +/// event are written — a handler may accept immediately via `group::accept_invite_for` (a +/// Package's auto-accept path). pub struct GroupInvitePayload { pub target_guid: u64, pub inviter_guid: u64, diff --git a/module/src/instance.rs b/module/src/instance.rs index d6c881798..236e94d4d 100644 --- a/module/src/instance.rs +++ b/module/src/instance.rs @@ -1025,7 +1025,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). +/// instance at once (bots count: a parked bot holds its instance open, correctly). /// /// Plus every instance claimed by a pending Transfer Intent or Escrow. A Transfer Intent removes /// the live body before the Gateway can begin Escrow, so both durable phases must hold the source diff --git a/module/src/loot/tag.rs b/module/src/loot/tag.rs index 91d34912e..4290abaf7 100644 --- a/module/src/loot/tag.rs +++ b/module/src/loot/tag.rs @@ -216,8 +216,8 @@ fn membership_is_current( } } -/// Read one live Loot Tag: tap membership alone, with no reward-distance check. The playerbots -/// Package calls it so a bot skips a creature another Character has tagged, and keeps fighting one +/// Read one live Loot Tag: tap membership alone, with no reward-distance check. A Package +/// calls it so a bot skips a creature another Character has tagged, and keeps fighting one /// it still has a claim on. Loot and quest credit read the same membership rule once, at death, /// through `death_entitlement`. The read is bounded at the Raid cap. #[cfg_attr(not(has_packages), allow(dead_code))] diff --git a/module/src/meeting_stone/mod.rs b/module/src/meeting_stone/mod.rs index b921c0d38..266e811c3 100644 --- a/module/src/meeting_stone/mod.rs +++ b/module/src/meeting_stone/mod.rs @@ -725,7 +725,7 @@ pub(crate) fn party_left(ctx: &ReducerContext, departed: PartyDeparture) -> Opti ); } dequeue_party(ctx, departed.group_id, queue_status::LEAVE_QUEUE); - // A kicked playerbot or offline member only leaves, as in cmangos (`if (player)`). + // A kicked bot or offline member only leaves, as in cmangos (`if (player)`). if !has_live_claim(ctx, guid) { return None; } diff --git a/module/src/package_config.rs b/module/src/package_config.rs index 678e9e897..31496acec 100644 --- a/module/src/package_config.rs +++ b/module/src/package_config.rs @@ -181,8 +181,8 @@ mod tests { #[test] fn the_refusal_names_the_packages_existing_keys_sorted() { let known = vec!["max_bots".to_string(), "spawn_rate".to_string()]; - let message = unknown_key_message("playerbots", "spwan_rate", &known); - assert!(message.contains("playerbots"), "{message}"); + let message = unknown_key_message("sample_package", "spwan_rate", &known); + assert!(message.contains("sample_package"), "{message}"); assert!(message.contains("spwan_rate"), "{message}"); assert!( message.contains("max_bots, spawn_rate"), diff --git a/module/src/seed/fixtures.rs b/module/src/seed/fixtures.rs index 081de8407..317e7e782 100644 --- a/module/src/seed/fixtures.rs +++ b/module/src/seed/fixtures.rs @@ -1456,7 +1456,7 @@ pub(crate) fn seed_scenario_fixtures(ctx: &ReducerContext) { if templates.entry().find(PROFESSION_TRAINER_ENTRY).is_none() { templates.insert(profession_trainer_template()); } - // "Test Wolf Elder" (51002) — the BOT-SUITE fight fixture (266). The playerbots tests level + // "Test Wolf Elder" (51002) — the BOT-SUITE fight fixture. The Package bot tests level // their bots to clear the cast level-gate (Taunt 355 = spell_level 10), which greys the L1 // Test Wolf 51000 (aggro_radius returns 0 at a >=8 level gap) — so its wolves stop aggroing and // the tank has nothing to Taunt. This one is level 9: non-grey to a level-10 bot (20-level gap diff --git a/module/src/trainer.rs b/module/src/trainer.rs index 48022dd62..8ffae0168 100644 --- a/module/src/trainer.rs +++ b/module/src/trainer.rs @@ -145,7 +145,7 @@ pub(crate) fn trainer_buy_check( /// E_SCRIPTED (never one of the excluded kinds), so it still resolves. This exclusion list MUST stay /// in lockstep with the importer's `wrapper_to_rank` heuristic (importer/src/spell.rs) — the two are /// the same rule on the two sides of the wire. Generic over the kind. Shared by `apply_trainer_buy` -/// (the player buy) and the playerbots trainer-kit pass (work-item 156) — ONE wrapper-resolution +/// (the player buy) and a Package's trainer-kit pass — ONE wrapper-resolution /// chokepoint, so a bot's spellbook and a trained player's can never drift. [entity] fn learn_target_trigger(effect: &crate::SpellEffect) -> Option { (effect.trigger_spell != 0 @@ -607,7 +607,7 @@ pub(crate) fn apply_trainer_buy( ) } else { // The castable RANK behind the offering — extracted to `resolve_learn_target` (see its doc) so - // the playerbots trainer-kit pass (work-item 156) resolves wrappers IDENTICALLY to a real buy. + // a Package's trainer-kit pass resolves wrappers IDENTICALLY to a real buy. // The already-known gate keys on the RANK so a re-buy of a known rank is rejected (not silently // re-charged). let to_learn = resolve_learn_target(ctx, spell_id); diff --git a/module/src/transfer/mod.rs b/module/src/transfer/mod.rs index cab356723..4e3d2333e 100644 --- a/module/src/transfer/mod.rs +++ b/module/src/transfer/mod.rs @@ -311,7 +311,7 @@ crate::character_owned!(delete, fn sweep_delete_game_transfer_out(ctx, character // Session-less crossings — the intent a Package writes and the Gateway executes // =========================================================================================== -/// A Shard crossing decided for a Character that has no Session: a playerbot whose party walked +/// A Shard crossing decided for a Character that has no Session: a bot whose party walked /// through a dungeon portal, and the same row again when the party walks back out. /// /// The escrowed transfer needs no client, but it does need a driver, and every driver we had was a diff --git a/module/src/world.rs b/module/src/world.rs index 49090b753..b3ac37c19 100644 --- a/module/src/world.rs +++ b/module/src/world.rs @@ -108,7 +108,7 @@ pub(crate) fn clear_relay_world_states_for_instance(ctx: &ReducerContext, instan // the 3-column planner limit does not apply to `helpers::entities_near`. index(accessor = by_cell, btree(columns = [map_id, instance_id, cell])), // Creature searches skip Character entry 0 before consuming their bounded read budget. The - // consumer is the playerbots Package's quest loop; nothing in Core reads this index. + // consumer is a Package's quest loop; nothing in Core reads this index. index(accessor = by_cell_entry, btree(columns = [map_id, instance_id, cell, entry])), // `entity_by_owner` is the auth prologue of ~77 player reducer call sites; without this it was a // full table scan per transaction (perf catalog 1.2). `owner_identity` never changes for a live From 40384736ce303e330684626375b5ef1cdd2eef95 Mon Sep 17 00:00:00 2001 From: Mathih13 Date: Thu, 8 Oct 2026 07:16:06 +0200 Subject: [PATCH 5/5] ci: build official Packages from a same-named packages branch when one exists Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/rust.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index fc1fe1320..1aca0a733 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -57,10 +57,20 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + # A paired packages PR uses the same branch name; build against it when it exists. + - id: packages-ref + env: + BRANCH: ${{ github.head_ref || github.ref_name }} + run: | + if git ls-remote --exit-code --heads https://github.com/LyraCoreProject/packages "refs/heads/$BRANCH" >/dev/null; then + echo "ref=$BRANCH" >> "$GITHUB_OUTPUT" + else + echo "ref=main" >> "$GITHUB_OUTPUT" + fi - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: LyraCoreProject/packages - ref: main + ref: ${{ steps.packages-ref.outputs.ref }} path: official-packages persist-credentials: false - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2