From 05153859bbc7057e1a89263faa11d17aa2365457 Mon Sep 17 00:00:00 2001 From: T3 Code Date: Tue, 8 Sep 2026 12:24:37 +0000 Subject: [PATCH 1/5] fix(playerbots): retain action outcomes and honest quest progress --- .github/workflows/core-tip.yml | 64 +++- playerbots/README.md | 55 ++++ playerbots/fixtures/actions.json | 48 +++ playerbots/src/actions.rs | 283 +++++++++++++++++ playerbots/src/fixture.rs | 520 +++++++++++++++++++++++++++++++ playerbots/src/goals.rs | 58 +++- playerbots/src/mod.rs | 7 + 7 files changed, 1015 insertions(+), 20 deletions(-) create mode 100644 playerbots/fixtures/actions.json create mode 100644 playerbots/src/actions.rs create mode 100644 playerbots/src/fixture.rs diff --git a/.github/workflows/core-tip.yml b/.github/workflows/core-tip.yml index ae988f5..3a4febe 100644 --- a/.github/workflows/core-tip.yml +++ b/.github/workflows/core-tip.yml @@ -37,11 +37,12 @@ jobs: with: path: collection - - name: Check out LyraCore main + - name: Check out the required LyraCore revision uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: LyraCoreProject/LyraCore - ref: main + # Restore main after the owning core operations merge. + ref: 5322de73ab06e557618a96b2f6609a9150b9c352 path: lyracore - name: Install Rust 1.93.0 @@ -61,11 +62,12 @@ jobs: with: path: collection - - name: Check out LyraCore main + - name: Check out the required LyraCore revision uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: LyraCoreProject/LyraCore - ref: main + # Restore main after the owning core operations merge. + ref: 5322de73ab06e557618a96b2f6609a9150b9c352 path: lyracore # `spacetime generate` builds the module to wasm and extracts its schema through it, so this @@ -105,3 +107,57 @@ jobs: run: | ./collection/.github/check-core-tip.sh "$GITHUB_WORKSPACE/lyracore" \ "$GITHUB_WORKSPACE/collection/.github/check-datascripts.sh" "$GITHUB_WORKSPACE/lyracore" + + playerbots-durable: + needs: firewall + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + SPACETIME_VERSION: 2.7.1 + SPACETIME_ARCHIVE_SHA256: 9b58192135733083479f30aba47dbbbc79b51db368c46cc221baa663e9c3a2c1 + steps: + - name: Check out Packages + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: collection + persist-credentials: false + - name: Check out the required LyraCore revision + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: LyraCoreProject/LyraCore + # Restore main after the owning core operations merge. + ref: 5322de73ab06e557618a96b2f6609a9150b9c352 + path: lyracore + persist-credentials: false + - name: Install the Rust toolchain and Wasm target + run: | + rustup toolchain install 1.93.0 --profile minimal + rustup target add wasm32-unknown-unknown --toolchain 1.93.0 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + workspaces: lyracore + - name: Install the pinned SpacetimeDB CLI + run: | + archive_path="$RUNNER_TEMP/spacetime.tar.gz" + cli_dir="$RUNNER_TEMP/spacetime-$SPACETIME_VERSION" + curl --fail --location --retry 3 \ + "https://github.com/clockworklabs/SpacetimeDB/releases/download/v$SPACETIME_VERSION/spacetime-x86_64-unknown-linux-gnu.tar.gz" \ + --output "$archive_path" + printf '%s %s\n' "$SPACETIME_ARCHIVE_SHA256" "$archive_path" | sha256sum --check --strict + mkdir -p "$cli_dir" + tar --extract --gzip --file "$archive_path" --directory "$cli_dir" + ln -s spacetimedb-cli "$cli_dir/spacetime" + echo "$cli_dir" >> "$GITHUB_PATH" + - name: Run the real Module and Package action fixture + run: | + cargo clean --manifest-path "$GITHUB_WORKSPACE/lyracore/Cargo.toml" -p lyracore-module + ./collection/.github/check-core-tip.sh "$GITHUB_WORKSPACE/lyracore" \ + cargo test --manifest-path "$GITHUB_WORKSPACE/lyracore/Cargo.toml" \ + -p lyracore-module --test playerbots_rewrite -- --ignored --test-threads=1 + - name: Upload fixture inputs and Standalone logs + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: playerbots-durable-evidence + path: /tmp/lyracore-standalone-logs + if-no-files-found: ignore diff --git a/playerbots/README.md b/playerbots/README.md index 8089f05..ececc6d 100644 --- a/playerbots/README.md +++ b/playerbots/README.md @@ -342,3 +342,58 @@ request, not a record: nothing refuses it and nothing retries it, so the deadlin the bot at all. Both leave the bot grinding with a kill objective it cannot serve, so a healthy bot in a thin spawn area can warn at a minute. Read its `game_character_quest` rows before acting on the warning; the fix would need a respawn timetable the Package has no read of. + +## Action fixture + +`module/tests/playerbots_rewrite.rs` in LyraCore drives real Package requests through a private +Standalone. Install this Package into that core checkout, then run: + +```sh +cargo clean -p lyracore-module +cargo test -p lyracore-module --test playerbots_rewrite -- --ignored --test-threads=1 +``` + +The input record is `fixtures/actions.json`. Each run records the tested core and collection commits, +whether either worktree is dirty, the Package Content Identity, and the Module wasm identity under +`/tmp/lyracore-standalone-logs`. Successful cases also save action, quest, cast, and position observations there. Content comes from core seeds and reserved fixture rows. Obstructions +come from synthetic navigation cells. No imported client geometry is present, and these cases do not +prove imported-world routing or real-client appearance. + +The fixture uses the existing gameplay operations. A Cast Handle correlates a scheduled spell with +its terminal outcome; accepting an attack does not certify a hit or quest credit. The bounded +`pkg_playerbots_action` table retains the latest observation of each action kind per Character. + +The rewrite adopts AzerothCore's decision grammar in Rust. The reference inventory at +`b949b50bfcdd4fab937781bac2d7765e39330e4b` counts 2,967 lines for the selection kernel and 243,560 for +the whole runtime. Even the kernel retains C++ host pointers and packet-bearing events. Porting that +runtime would require synthetic WotLK World Sessions, packet queues, and host objects before replacing +them with LyraCore's Module operations. Keeping typed decisions and durable identifiers beside the +existing cast, combat, navigation, quest, and item operations is the smaller total implementation. +The counts describe the reference source, not an estimate of the Rust rewrite. + +When sharing `CARGO_TARGET_DIR` between checkouts, clean the Module after changing the worktree or +installed Package inventory. Its build script discovers Package files and hooks. A shared cache can +otherwise reuse discovery output from the previous checkout. Dependency artifacts can stay cached. + +Cast observations retain the original Cast Handle while waiting. Instant effects resolve in the +request itself, before any scheduled completion callback. `CastResolved` means effects dispatched; +a projectile can still be in flight. Channel requests explicitly return `UnsupportedChannel` until +channel ownership is implemented. Targeted casts currently have no shared line-of-sight Gate; this +fixture does not invent a line-of-sight refusal. Actor requests also retain the existing spellbook +bypass; later capability selection must require known, supported spells. + +Movement observations retain complete, partial, blocked, and direct planning status, clipping, +coverage, and a last-advance time measured from successive actual positions. Complete planning is +separate from arrival. Attack acceptance is separate from quest credit; the quest progress clock +resets only when objective counts increase or a synchronous quest operation succeeds. + +Quest interactions finish synchronously. The core plans collect-item consumption and all rewards +before changing inventory, so a caught turn-in Refusal leaves items, money, experience, and the quest +unchanged. The fixture covers an invalid reward choice, full inventory, successful retry after a +slot is freed, and a source-item acceptance refusal. Imported item uniqueness limits now apply to +storage and reward exchanges, including bank holdings and other rewards in the same exchange. + +`pkg_playerbots_goal.quest_credit` is an end-appended nullable count with a null default. The first +observation establishes its baseline without clearing an existing stall. Later increases count as +progress. The fixture exercises an existing stalled goal with nonzero objective credit and this +migration default; it does not claim to publish an older Package binary before upgrading it. diff --git a/playerbots/fixtures/actions.json b/playerbots/fixtures/actions.json new file mode 100644 index 0000000..8f8238b --- /dev/null +++ b/playerbots/fixtures/actions.json @@ -0,0 +1,48 @@ +{ + "fixture": "playerbots-actions-v1", + "baseline_core": "95e6ad8c61540f74471e3087897e8c3ef7e065d1", + "baseline_collection": "c32b2fb7aa405190728034b63ce6eb9a3c0dd6be", + "reference": "b949b50bfcdd4fab937781bac2d7765e39330e4b", + "content": { + "revision": "playerbots-seeded-actions-v1", + "provenance": "core seeded templates and reserved fixture rows", + "heal_source_spell": 2050, + "creature_source_entry": 51000, + "item_source": "first starter item held by the seeded role-1 bot" + }, + "imported_content": null, + "geometry": { + "revision": "playerbots-synthetic-nav-v1", + "provenance": "synthetic walk and obstruction arrays", + "client_geometry": null, + "blocked": "3 by 3 navigation cells, walk bytes 0, obs bytes 20, base_z 50", + "partial": "9 cells along y, one nonwalkable x sub-cell at 1210, other walk bits 1, obs bytes 255" + }, + "spacetimedb": "2.7.1", + "rust": "1.93.0", + "map": 0, + "instance": 0, + "position": [ + 1200, + 1200, + 50 + ], + "heal": { + "spell": 5090100, + "source_spell": 2050, + "cast_time_ms": 5000, + "cost": 0 + }, + "poor_range_target_x": 1400, + "quest": { + "hunt": 50909, + "target_entry": 5090101, + "required_kills": 2, + "interaction": 50910, + "collect_item": 5090120, + "guaranteed_reward": 5090121, + "choice_reward": 5090122, + "filler_item": 5090123, + "full_inventory_slots": 16 + } +} diff --git a/playerbots/src/actions.rs b/playerbots/src/actions.rs new file mode 100644 index 0000000..7e16044 --- /dev/null +++ b/playerbots/src/actions.rs @@ -0,0 +1,283 @@ +//! Bounded action observations. Acceptance never implies damage or quest credit. + +use crate::spell::{CastFinish, CastHandle, CastRefusal, CastStart}; +use spacetimedb::{table, ReducerContext, Table}; + +#[derive(spacetimedb::SpacetimeType, Clone, Copy, Debug, PartialEq, Eq)] +pub enum ActionKind { + Cast, + Attack, + Move, + AcceptQuest, + TurnInQuest, +} + +#[derive(spacetimedb::SpacetimeType, Clone, Debug)] +pub enum ActionOutcome { + AttackAccepted(crate::combat::AttackStart), + Completed, + Movement(MovementObservation), + CastResolved, + Waiting(CastHandle), + Refused(crate::actor::ActionRefusal), + CastRefused(CastRefusal), + Cancelled, + Expired, +} + +/// At most one observation per action kind per Character. Repeated waiting retains its start. +#[table(accessor = pkg_playerbots_action, public, + index(accessor = by_character, btree(columns = [character_guid])))] +pub struct PlayerbotsAction { + #[primary_key] + #[auto_inc] + pub id: u64, + pub character_guid: u64, + pub kind: ActionKind, + pub target_guid: u64, + pub spell_id: u32, + pub quest_entry: u32, + pub cast_id: u64, + pub outcome: ActionOutcome, + pub started_micros: i64, + pub observed_micros: i64, +} + +crate::character_owned!(delete, fn sweep_delete_pkg_playerbots_action(ctx, character_guid) { + let rows = ctx.db.pkg_playerbots_action(); + for row in rows.by_character().filter(character_guid).collect::>() { + rows.id().delete(row.id); + } +}); +crate::character_owned!(not_transported, fn sweep_transfer_pkg_playerbots_action()); + +fn prior(ctx: &ReducerContext, guid: u64, kind: ActionKind) -> Option { + ctx.db + .pkg_playerbots_action() + .by_character() + .filter(guid) + .find(|r| r.kind == kind) +} + +fn record(ctx: &ReducerContext, mut row: PlayerbotsAction) { + let rows = ctx.db.pkg_playerbots_action(); + if let Some(before) = prior(ctx, row.character_guid, row.kind) { + row.id = before.id; + if row.cast_id != 0 && row.cast_id == before.cast_id { + row.started_micros = before.started_micros; + } + rows.id().update(row); + } else { + rows.insert(row); + } +} + +pub(super) fn attack( + ctx: &ReducerContext, + guid: u64, + target_guid: u64, +) -> Result { + let result = crate::actor::request_attack(ctx, guid, target_guid); + let outcome = match &result { + Ok(accepted) => ActionOutcome::AttackAccepted(*accepted), + Err(reason) => ActionOutcome::Refused(reason.clone()), + }; + let now = ctx.timestamp.to_micros_since_unix_epoch(); + record( + ctx, + PlayerbotsAction { + id: 0, + character_guid: guid, + kind: ActionKind::Attack, + target_guid, + quest_entry: 0, + spell_id: 0, + cast_id: 0, + outcome, + started_micros: now, + observed_micros: now, + }, + ); + result +} + +pub(super) fn cast( + ctx: &ReducerContext, + guid: u64, + spell_id: u32, + target_guid: u64, +) -> Result { + let result = crate::actor::request_cast(ctx, guid, spell_id, target_guid); + let (outcome, cast_id, spell_id, target_guid) = match &result { + Ok(CastStart::Started(handle) | CastStart::Waiting(handle)) => ( + ActionOutcome::Waiting(handle.clone()), + handle.scheduled_id, + handle.spell_id, + handle.target_guid, + ), + Ok(CastStart::Resolved) => (ActionOutcome::CastResolved, 0, spell_id, target_guid), + Err(reason) => ( + ActionOutcome::CastRefused(reason.clone()), + 0, + spell_id, + target_guid, + ), + }; + let now = ctx.timestamp.to_micros_since_unix_epoch(); + record( + ctx, + PlayerbotsAction { + id: 0, + character_guid: guid, + kind: ActionKind::Cast, + target_guid, + quest_entry: 0, + spell_id, + cast_id, + outcome, + started_micros: now, + observed_micros: now, + }, + ); + result +} + +crate::game_hook!(on_cast_finished, fn playerbots_cast_finished(ctx, payload) { + let Some(mut row) = prior(ctx, payload.caster_guid, ActionKind::Cast) else { return; }; + if row.cast_id != payload.scheduled_id { return; } + row.outcome = match &payload.outcome { + CastFinish::Resolved => ActionOutcome::CastResolved, + CastFinish::Refused(reason) => ActionOutcome::CastRefused(reason.clone()), + CastFinish::Cancelled => ActionOutcome::Cancelled, + CastFinish::Expired => ActionOutcome::Expired, + }; + row.observed_micros = ctx.timestamp.to_micros_since_unix_epoch(); + ctx.db.pkg_playerbots_action().id().update(row); +}); + +fn interaction( + ctx: &ReducerContext, + guid: u64, + giver: u64, + quest: u32, + kind: ActionKind, + result: Result<(), crate::actor::ActionRefusal>, +) -> Result<(), crate::actor::ActionRefusal> { + let outcome = match &result { + Ok(()) => ActionOutcome::Completed, + Err(reason) => ActionOutcome::Refused(reason.clone()), + }; + let now = ctx.timestamp.to_micros_since_unix_epoch(); + record( + ctx, + PlayerbotsAction { + id: 0, + character_guid: guid, + kind, + target_guid: giver, + spell_id: 0, + quest_entry: quest, + cast_id: 0, + outcome, + started_micros: now, + observed_micros: now, + }, + ); + result +} + +pub(super) fn accept_quest( + ctx: &ReducerContext, + guid: u64, + giver: u64, + quest: u32, +) -> Result<(), crate::actor::ActionRefusal> { + interaction( + ctx, + guid, + giver, + quest, + ActionKind::AcceptQuest, + crate::actor::request_accept_quest(ctx, guid, giver, quest), + ) +} + +pub(super) fn turn_in_quest( + ctx: &ReducerContext, + guid: u64, + giver: u64, + quest: u32, + reward: u32, +) -> Result<(), crate::actor::ActionRefusal> { + interaction( + ctx, + guid, + giver, + quest, + ActionKind::TurnInQuest, + crate::actor::request_turn_in_quest(ctx, guid, giver, quest, reward), + ) +} + +/// Planning evidence plus progress measured at the next request, never at a proposed endpoint. +#[derive(spacetimedb::SpacetimeType, Clone, Debug)] +pub struct MovementObservation { + pub map_id: u32, + pub instance_id: u64, + pub destination: crate::nav::RoutePoint, + pub route: crate::nav::RouteStep, + pub arrived: bool, + pub last_advance_micros: Option, +} + +pub(super) fn movement( + ctx: &ReducerContext, + guid: u64, + map_id: u32, + instance_id: u64, + destination: crate::nav::RoutePoint, + arrived: bool, + route: crate::nav::RouteStep, +) { + let now = ctx.timestamp.to_micros_since_unix_epoch(); + let mut last_advance_micros = None; + if let Some(before) = prior(ctx, guid, ActionKind::Move) { + if let ActionOutcome::Movement(previous) = before.outcome { + if previous.map_id == map_id + && previous.instance_id == instance_id + && previous.destination == destination + { + let dx = route.from.x - previous.route.from.x; + let dy = route.from.y - previous.route.from.y; + last_advance_micros = if dx * dx + dy * dy > 0.05 * 0.05 { + Some(now) + } else { + previous.last_advance_micros + }; + } + } + } + let outcome = ActionOutcome::Movement(MovementObservation { + map_id, + instance_id, + destination, + route, + arrived, + last_advance_micros, + }); + record( + ctx, + PlayerbotsAction { + id: 0, + character_guid: guid, + kind: ActionKind::Move, + target_guid: 0, + spell_id: 0, + quest_entry: 0, + cast_id: 0, + outcome, + started_micros: now, + observed_micros: now, + }, + ); +} diff --git a/playerbots/src/fixture.rs b/playerbots/src/fixture.rs new file mode 100644 index 0000000..1e22565 --- /dev/null +++ b/playerbots/src/fixture.rs @@ -0,0 +1,520 @@ +//! Deterministic staging for the private durable action fixture. + +use super::pkg_playerbots_personality; +use super::{pkg_playerbots_bot, PlayerbotsBot}; +use crate::nav::game_nav_chunk; +use crate::{ + game_creature_spawn, game_creature_template, game_quest_objective, game_quest_template, +}; +use crate::{game_creature_spline, game_spell, game_spell_effect, game_world_entity}; +use spacetimedb::{reducer, ReducerContext, Table}; + +const HEAL: u32 = 5_090_100; + +#[reducer] +pub fn playerbots_fixture_prepare(ctx: &ReducerContext) -> Result<(), String> { + crate::helpers::require_operator(ctx)?; + let bots: Vec = ctx.db.pkg_playerbots_bot().iter().collect(); + for mut bot in bots { + bot.next_think_micros = i64::MAX; + ctx.db + .game_creature_spline() + .guid() + .delete(bot.character_guid); + let mut entity = crate::helpers::live_entity(ctx, bot.character_guid)?; + entity.health = 1; + entity.x = 1200.0; + entity.y = 1200.0; + entity.z = 50.0; + let (gx, gy) = lyracore_shared::spatial::grid_cell(entity.x, entity.y); + entity.grid_x = gx; + entity.grid_y = gy; + entity.cell = lyracore_shared::spatial::grid_cell_id(gx, gy); + ctx.db.game_world_entity().guid().update(entity); + ctx.db.pkg_playerbots_bot().id().update(bot); + } + let mut spell = ctx + .db + .game_spell() + .spell_id() + .find(2050) + .ok_or("seed heal missing")?; + spell.spell_id = HEAL; + spell.cast_time_ms = 5000; + spell.cost = 0; + spell.cooldown_ms = 0; + ctx.db.game_spell().spell_id().delete(HEAL); + ctx.db.game_spell().insert(spell); + for mut effect in ctx + .db + .game_spell_effect() + .by_spell() + .filter(2050u32) + .collect::>() + { + effect.spell_id = HEAL; + effect.id = ((HEAL as u64) << 2) | effect.effect_index as u64; + ctx.db.game_spell_effect().id().delete(effect.id); + ctx.db.game_spell_effect().insert(effect); + } + Ok(()) +} + +#[reducer] +pub fn playerbots_fixture_cast( + ctx: &ReducerContext, + caster: u64, + target: u64, +) -> Result<(), String> { + crate::helpers::require_operator(ctx)?; + let _ = super::actions::cast(ctx, caster, HEAL, target); + Ok(()) +} + +#[reducer] +pub fn playerbots_fixture_resume(ctx: &ReducerContext, guid: u64) -> Result<(), String> { + crate::helpers::require_operator(ctx)?; + let mut bot = ctx + .db + .pkg_playerbots_bot() + .by_character() + .filter(guid) + .next() + .ok_or("bot missing")?; + bot.next_think_micros = 0; + ctx.db.pkg_playerbots_bot().id().update(bot); + let mut personality = ctx + .db + .pkg_playerbots_personality() + .by_character() + .filter(guid) + .next() + .ok_or("personality missing")?; + personality.flee_at_pct = 0; + ctx.db.pkg_playerbots_personality().id().update(personality); + Ok(()) +} + +#[reducer] +pub fn playerbots_fixture_cancel( + ctx: &ReducerContext, + guid: u64, + expired: bool, +) -> Result<(), String> { + crate::helpers::require_operator(ctx)?; + if let Some(cast) = crate::spell::pending_cast(ctx, guid) { + if expired { + crate::spell::expire_cast_attempt( + ctx, + guid, + cast.scheduled_id, + ctx.timestamp.to_micros_since_unix_epoch(), + ); + } else { + crate::spell::cancel_cast_attempt(ctx, guid, cast.scheduled_id); + } + } + Ok(()) +} + +#[reducer] +pub fn playerbots_fixture_position(ctx: &ReducerContext, guid: u64, x: f32) -> Result<(), String> { + crate::helpers::require_operator(ctx)?; + let mut entity = crate::helpers::live_entity(ctx, guid)?; + entity.x = x; + let (gx, gy) = lyracore_shared::spatial::grid_cell(entity.x, entity.y); + entity.grid_x = gx; + entity.grid_y = gy; + entity.cell = lyracore_shared::spatial::grid_cell_id(gx, gy); + ctx.db.game_world_entity().guid().update(entity); + Ok(()) +} + +#[reducer] +pub fn playerbots_fixture_blocked_quest(ctx: &ReducerContext, guid: u64) -> Result<(), String> { + crate::helpers::require_operator(ctx)?; + let mut me = crate::helpers::live_entity(ctx, guid)?; + me.health = me.max_health; + ctx.db.game_world_entity().guid().update(me); + let me = crate::helpers::live_entity(ctx, guid)?; + let entry = 5_090_101; + let target_guid = (0xF130u64 << 48) | ((entry as u64) << 24) | 1; + let mut template = ctx + .db + .game_creature_template() + .entry() + .find(51000) + .ok_or("seed wolf missing")?; + template.entry = entry; + ctx.db.game_creature_template().entry().delete(entry); + let template = ctx.db.game_creature_template().insert(template); + let spawn = crate::CreatureSpawn { + guid: target_guid, + entry, + map_id: me.map_id, + x: me.x + 3.0, + y: me.y, + z: me.z, + orientation: 0.0, + respawn_at: ctx.timestamp, + despawn_at: ctx.timestamp, + movement_type: 0, + respawn_secs: 60, + life_seq: 1, + }; + ctx.db.game_creature_spawn().guid().delete(target_guid); + let spawn = ctx.db.game_creature_spawn().insert(spawn); + crate::creatures::insert_creature_entity( + ctx, + crate::creatures::build_creature_entity(&spawn, &template, 0, 0), + ); + ctx.db.game_quest_template().entry().delete(50909); + ctx.db.game_quest_template().insert(crate::QuestTemplate { + entry: 50909, + min_level: 1, + quest_level: 1, + title: "Blocked fixture hunt".to_string(), + reward_money: 0, + reward_xp: 0, + prev_quest_id: 0, + required_races: 0, + required_classes: 0, + zone_or_sort: 0, + rew_rep_faction_1: 0, + rew_rep_value_1: 0, + rew_rep_faction_2: 0, + rew_rep_value_2: 0, + src_item: 0, + src_item_count: 0, + repeatable: false, + next_quest_id: 0, + limit_time: 0, + reward_money_max_level: 0, + }); + ctx.db.game_quest_objective().id().delete(5_090_101); + ctx.db.game_quest_objective().insert(crate::QuestObjective { + id: 5_090_101, + quest_entry: 50909, + obj_index: 0, + kind: crate::quest::objective_kind::KILL_CREATURE, + target_entry: entry, + required_count: 2, + }); + crate::actor::stage_quest(ctx, guid, 50909)?; + let cx = lyracore_shared::terrain::cell_index(me.x).ok_or("fixture off grid")?; + let cy = lyracore_shared::terrain::cell_index(me.y).ok_or("fixture off grid")?; + for x in cx - 1..=cx + 1 { + for y in cy - 1..=cy + 1 { + let key = lyracore_shared::terrain::cell_key(me.map_id, x, y); + ctx.db.game_nav_chunk().key().delete(key); + ctx.db.game_nav_chunk().insert(crate::nav::NavChunk { + key, + map_id: me.map_id, + cell_x: x, + cell_y: y, + base_z: me.z, + walk: vec![0; lyracore_shared::nav::WALK_BYTES], + obs: vec![20; lyracore_shared::nav::OBS_BYTES], + }); + } + } + if crate::nav::has_los( + ctx, + me.map_id, + me.instance_id, + (me.x, me.y, me.z), + (spawn.x, spawn.y, spawn.z), + ) { + return Err("fixture ray unexpectedly clear".to_string()); + } + playerbots_fixture_resume(ctx, guid) +} + +#[reducer] +pub fn playerbots_fixture_move(ctx: &ReducerContext, guid: u64, x: f32) -> Result<(), String> { + crate::helpers::require_operator(ctx)?; + let me = crate::helpers::live_entity(ctx, guid)?; + super::goals::walk_toward(ctx, &me, (x, me.y, me.z), 0.0, true); + Ok(()) +} + +#[reducer] +pub fn playerbots_fixture_cast_mode( + ctx: &ReducerContext, + caster: u64, + target: u64, + channel: bool, +) -> Result<(), String> { + crate::helpers::require_operator(ctx)?; + let mut spell = ctx + .db + .game_spell() + .spell_id() + .find(HEAL) + .ok_or("fixture heal missing")?; + spell.cast_time_ms = 0; + spell.cast_flags = if channel { + crate::spell::SPELL_ATTR_CHANNELED + } else { + 0 + }; + ctx.db.game_spell().spell_id().update(spell); + let _ = super::actions::cast(ctx, caster, HEAL, target); + Ok(()) +} + +#[reducer] +pub fn playerbots_fixture_cancel_handle( + ctx: &ReducerContext, + guid: u64, + scheduled_id: u64, + deadline: i64, +) -> Result<(), String> { + crate::helpers::require_operator(ctx)?; + if deadline == 0 { + crate::spell::cancel_cast_attempt(ctx, guid, scheduled_id); + } else { + crate::spell::expire_cast_attempt(ctx, guid, scheduled_id, deadline); + } + Ok(()) +} + +#[reducer] +pub fn playerbots_fixture_attack( + ctx: &ReducerContext, + guid: u64, + target: u64, +) -> Result<(), String> { + crate::helpers::require_operator(ctx)?; + let _ = super::actions::attack(ctx, guid, target); + Ok(()) +} + +#[reducer] +pub fn playerbots_fixture_partial_route(ctx: &ReducerContext, guid: u64) -> Result<(), String> { + crate::helpers::require_operator(ctx)?; + let me = crate::helpers::live_entity(ctx, guid)?; + let wall_x = me.x + 10.0; + let cx = lyracore_shared::terrain::cell_index(wall_x).ok_or("fixture off grid")?; + let cy = lyracore_shared::terrain::cell_index(me.y).ok_or("fixture off grid")?; + let nx = lyracore_shared::nav::sub_index(wall_x, cx, lyracore_shared::nav::WALK_DIM) + .ok_or("fixture wall off grid")?; + for y in cy - 4..=cy + 4 { + let key = lyracore_shared::terrain::cell_key(me.map_id, cx, y); + let mut walk = vec![255; lyracore_shared::nav::WALK_BYTES]; + for ny in 0..lyracore_shared::nav::WALK_DIM { + lyracore_shared::nav::walk_set(&mut walk, nx, ny, false); + } + ctx.db.game_nav_chunk().key().delete(key); + ctx.db.game_nav_chunk().insert(crate::nav::NavChunk { + key, + map_id: me.map_id, + cell_x: cx, + cell_y: y, + base_z: me.z, + walk, + obs: vec![lyracore_shared::nav::OBS_NONE; lyracore_shared::nav::OBS_BYTES], + }); + } + Ok(()) +} + +const QUEST: u32 = 50910; +const COLLECT: u32 = 5_090_120; +const GUARANTEED: u32 = 5_090_121; +const CHOICE: u32 = 5_090_122; +const FILLER: u32 = 5_090_123; + +fn fixture_giver() -> u64 { + (0xF130u64 << 48) | (5_090_101u64 << 24) | 1 +} + +#[reducer] +pub fn playerbots_fixture_interaction_stage( + ctx: &ReducerContext, + guid: u64, + source_item: bool, +) -> Result<(), String> { + crate::helpers::require_operator(ctx)?; + playerbots_fixture_blocked_quest(ctx, guid)?; + let mut bot = ctx + .db + .pkg_playerbots_bot() + .by_character() + .filter(guid) + .next() + .ok_or("bot missing")?; + bot.next_think_micros = i64::MAX; + ctx.db.pkg_playerbots_bot().id().update(bot); + use crate::{ + game_character_quest, game_creature_quest, game_item_instance, game_item_template, + game_quest_reward_choice, game_quest_reward_item, + }; + let original_item = ctx + .db + .game_item_instance() + .by_owner_guid() + .filter(guid) + .next() + .ok_or("starter item missing")?; + for entry in [COLLECT, GUARANTEED, CHOICE, FILLER] { + let mut template = ctx + .db + .game_item_template() + .entry() + .find(original_item.entry) + .ok_or("starter template missing")?; + template.entry = entry; + template.max_stack = 1; + template.max_count = 0; + template.random_property = 0; + ctx.db.game_item_template().entry().delete(entry); + ctx.db.game_item_template().insert(template); + } + for item in ctx + .db + .game_item_instance() + .by_owner_guid() + .filter(guid) + .collect::>() + { + ctx.db.game_item_instance().guid().delete(item.guid); + } + for quest in ctx + .db + .game_character_quest() + .by_character() + .filter(guid) + .collect::>() + { + ctx.db.game_character_quest().id().delete(quest.id); + } + let mut template = ctx + .db + .game_quest_template() + .entry() + .find(50909) + .ok_or("fixture quest missing")?; + template.entry = QUEST; + template.title = "Inventory exchange fixture".to_string(); + template.reward_money = 150; + template.reward_xp = 90; + template.src_item = if source_item { COLLECT } else { 0 }; + template.src_item_count = 1; + ctx.db.game_quest_template().entry().delete(QUEST); + ctx.db.game_quest_template().insert(template); + ctx.db.game_quest_objective().insert(crate::QuestObjective { + id: COLLECT as u64, + quest_entry: QUEST, + obj_index: 0, + kind: crate::quest::objective_kind::COLLECT_ITEM, + target_entry: COLLECT, + required_count: 1, + }); + for role in [ + crate::quest::quest_role::START, + crate::quest::quest_role::END, + ] { + ctx.db.game_creature_quest().insert(crate::CreatureQuest { + id: 0, + creature_entry: 5_090_101, + quest_entry: QUEST, + role, + }); + } + ctx.db + .game_quest_reward_item() + .insert(crate::QuestRewardItem { + id: GUARANTEED as u64, + quest_entry: QUEST, + item_entry: GUARANTEED, + count: 1, + }); + ctx.db + .game_quest_reward_choice() + .insert(crate::QuestRewardChoice { + id: CHOICE as u64, + quest_entry: QUEST, + choice_index: 0, + item_entry: CHOICE, + count: 1, + }); + if !source_item { + crate::actor::stage_quest(ctx, guid, QUEST)?; + crate::items::grant_item(ctx, guid, COLLECT, 1)?; + } + crate::items::grant_item(ctx, guid, FILLER, if source_item { 16 } else { 15 })?; + Ok(()) +} + +#[reducer] +pub fn playerbots_fixture_interact( + ctx: &ReducerContext, + guid: u64, + accept: bool, + choice: u32, +) -> Result<(), String> { + crate::helpers::require_operator(ctx)?; + if accept { + let _ = super::actions::accept_quest(ctx, guid, fixture_giver(), QUEST); + } else { + let _ = super::actions::turn_in_quest(ctx, guid, fixture_giver(), QUEST, choice); + } + Ok(()) +} + +#[reducer] +pub fn playerbots_fixture_free_slot(ctx: &ReducerContext, guid: u64) -> Result<(), String> { + crate::helpers::require_operator(ctx)?; + crate::items::remove_items(ctx, guid, FILLER, 1) +} + +#[reducer] +pub fn playerbots_fixture_freeze(ctx: &ReducerContext, guid: u64) -> Result<(), String> { + crate::helpers::require_operator(ctx)?; + let mut bot = ctx + .db + .pkg_playerbots_bot() + .by_character() + .filter(guid) + .next() + .ok_or("bot missing")?; + bot.next_think_micros = i64::MAX; + ctx.db.pkg_playerbots_bot().id().update(bot); + Ok(()) +} + +#[reducer] +pub fn playerbots_fixture_forget_credit(ctx: &ReducerContext, guid: u64) -> Result<(), String> { + crate::helpers::require_operator(ctx)?; + use super::pkg_playerbots_goal; + let mut goal = ctx + .db + .pkg_playerbots_goal() + .by_character() + .filter(guid) + .next() + .ok_or("goal missing")?; + goal.quest_credit = None; + ctx.db.pkg_playerbots_goal().id().update(goal); + Ok(()) +} + +#[reducer] +pub fn playerbots_fixture_credit_kill(ctx: &ReducerContext, guid: u64) -> Result<(), String> { + crate::helpers::require_operator(ctx)?; + let target = fixture_giver(); + let (amount, _) = crate::combat::fold_incoming_damage(ctx, guid, target, 10000); + let damage = crate::combat::final_damage(ctx, target, amount); + let outcome = crate::combat::apply_hit( + ctx, + guid, + target, + damage, + crate::combat::Hit::weapon(crate::combat::HitSource::MainHand, false), + ); + if !outcome.killed { + return Err("fixture hit was not lethal".to_string()); + } + Ok(()) +} diff --git a/playerbots/src/goals.rs b/playerbots/src/goals.rs index 45e8702..414e396 100644 --- a/playerbots/src/goals.rs +++ b/playerbots/src/goals.rs @@ -232,12 +232,19 @@ fn think(ctx: &ReducerContext, bot: &PlayerbotsBot, now: i64) { me.max_health, flee_threshold(ctx, &me, personality.flee_at_pct), ) { + if let Some(cast) = crate::spell::pending_cast(ctx, me.guid) { + crate::spell::cancel_cast_attempt(ctx, me.guid, cast.scheduled_id); + } let _ = crate::actor::stop_attack(ctx, me.guid); walk_toward(ctx, &me, (bot.home_x, bot.home_y, bot.home_z), 0.0, true); record_goal(ctx, bot.character_guid, goal::FLEE, now); return; } + if crate::spell::pending_cast(ctx, me.guid).is_some() { + return; + } + let party = party_of(ctx, bot.character_guid); let leader = party .as_ref() @@ -423,6 +430,7 @@ fn record_goal(ctx: &ReducerContext, character_guid: u64, kind: u8, now: i64) { hub_x: 0.0, hub_y: 0.0, hub_z: 0.0, + quest_credit: None, }); } } @@ -1221,7 +1229,7 @@ fn fight( target: u64, ) { if closes_to_melee(bot.role, party.is_some()) { - let _ = crate::actor::attack(ctx, me.guid, target); + let _ = super::actions::attack(ctx, me.guid, target); // Nothing chases for a PLAYER-typed entity, so a bot that pulled from range would stand // there swinging at nothing. Close first, cast second. if let Ok(enemy) = crate::helpers::live_entity(ctx, target) { @@ -1272,7 +1280,7 @@ fn cast_rotation( }) else { continue; }; - if crate::actor::cast_at(ctx, me.guid, row.spell_id, cast_at).is_ok() { + if super::actions::cast(ctx, me.guid, row.spell_id, cast_at).is_ok() { return; } } @@ -1597,9 +1605,23 @@ fn quest( take_what_the_kill_left(ctx, me, &sight, &wanted_items(ctx, &active)); let decided = decide_quest(ctx, me, bot, personality, engaged, &active, &sight, now); - let work = decided + let mut work = decided .as_ref() .map_or(QuestWork::NoProgress, |step| step.work); + let credit = active + .iter() + .flat_map(|quest| &quest.counts) + .map(|&count| u64::from(count)) + .sum(); + if let Some(mut row) = goal_row(ctx, me.guid, now) { + if row.quest_credit.is_some_and(|previous| credit > previous) { + work = QuestWork::Progress; + } + if row.quest_credit != Some(credit) { + row.quest_credit = Some(credit); + ctx.db.pkg_playerbots_goal().id().update(row); + } + } keep_stall_clock(ctx, me.guid, work, active.len(), now); if ungrouped { invite_a_fellow_quester(ctx, me, &active, &sight, now); @@ -1656,7 +1678,7 @@ fn engaged_reason( .iter() .any(|cq| kill_target_entry(ctx, cq) == Some(entry)) { - return Some(QuestStep::progress(goal::QUEST_HUNT)); + return Some(QuestStep::no_progress(goal::QUEST_HUNT)); } (held == Some(goal::GRIND)).then(|| QuestStep::no_progress(goal::GRIND)) } @@ -1767,7 +1789,7 @@ fn work_quest( && live_target_is_available(ctx, me.guid, e) })?; fight(ctx, me, bot, None, personality, target.guid); - return Some(QuestStep::progress(goal::QUEST_HUNT)); + return Some(QuestStep::no_progress(goal::QUEST_HUNT)); } if !turn_in_ready(ctx, cq, now) { return None; @@ -1839,7 +1861,7 @@ fn hand_it_back( } // Reward index 0: this Package takes the quest's guaranteed rewards and the first of any // choice, because a bot has no gear plan to pick against. - match crate::actor::turn_in_quest(ctx, me.guid, ender.guid, quest_entry, 0) { + match super::actions::turn_in_quest(ctx, me.guid, ender.guid, quest_entry, 0) { Ok(()) => { spacetimedb::log::info!("playerbots: bot {} turned in quest {quest_entry}", me.guid); Some(QuestStep::progress(goal::QUEST_TRAVEL)) @@ -1850,7 +1872,7 @@ fn hand_it_back( // again next tick and gets in. When it does not, the stall clock is what records it; a // warning every second about a state the Operator can read off the bag would drown the log // that carries the real drift below. - Err(refusal) if refusal == lyracore_shared::mail::INVENTORY_FULL => None, + Err(refusal) if refusal.kind == crate::actor::ActionRefusalKind::InventoryFull => None, // The bot only walks here once [`turn_in_ready`] says yes, so any other Refusal means that // reading and the core have disagreed. Said out loud, for the same reason a refused accept // is: it is a defect, not a gameplay outcome. @@ -1900,7 +1922,7 @@ fn take_a_quest( return Some(QuestStep::no_progress(goal::QUEST_TRAVEL)); } for quest_entry in open_quests_of(ctx, me, giver.entry, &reach) { - match crate::actor::accept_quest(ctx, me.guid, giver.guid, quest_entry) { + match super::actions::accept_quest(ctx, me.guid, giver.guid, quest_entry) { Ok(()) => { // Bookmark the giver, not the bot: the bot is standing within interaction range of // it, but the giver is the thing that will still be here when the quest is done. @@ -2380,7 +2402,7 @@ pub(crate) fn wander_offset(character_guid: u64, window: i64) -> (f32, f32) { /// One movement leg toward `dest`, stopping `stand_off` yards short of it, capped at what the bot /// can cover before it next thinks. Writes the same `game_creature_spline` row every creature leg /// writes, so the Gateway relays it through the one movement path it already has. -fn walk_toward( +pub(super) fn walk_toward( ctx: &ReducerContext, me: &crate::WorldEntity, dest: (f32, f32, f32), @@ -2394,13 +2416,7 @@ fn walk_toward( }; let full = distance_2d(me.x, me.y, dest.0, dest.1); let step = step_length(full, stand_off, speed, THINK_INTERVAL_MICROS); - if step <= 0.0 { - return; - } - // The same leg a creature walks: one nav-grid A* step toward `dest`, held off walls by the - // collision gate. A step the gate refuses leaves the bot where it is, with no spline, and the - // stall clock says so. - let (lx, ly) = crate::nav::nav_step( + let route = crate::nav::route_step( ctx, me.map_id, me.instance_id, @@ -2410,6 +2426,16 @@ fn walk_toward( stand_off, me.z, ); + let (lx, ly) = (route.endpoint.x, route.endpoint.y); + super::actions::movement( + ctx, + me.guid, + me.map_id, + me.instance_id, + (dest.0, dest.1).into(), + full <= stand_off + 0.05, + route, + ); let travelled = distance_2d(me.x, me.y, lx, ly); if travelled <= 0.0 { return; diff --git a/playerbots/src/mod.rs b/playerbots/src/mod.rs index 1802a4b..e374aeb 100644 --- a/playerbots/src/mod.rs +++ b/playerbots/src/mod.rs @@ -18,7 +18,11 @@ use spacetimedb::{reducer, table, Identity, ReducerContext, Table}; +mod actions; mod goals; +pub(crate) use actions::*; +#[cfg(feature = "debug_reducers")] +mod fixture; pub(crate) use goals::*; use crate::package_config::game_package_config; @@ -216,6 +220,9 @@ pub struct PlayerbotsGoal { /// End-appended with a default, so a published Shard migrates in place. #[default(false)] pub stall_warned: bool, + /// Last observed objective total. None initializes a baseline without clearing an existing stall. + #[default(None::)] + pub quest_credit: Option, } crate::character_owned!(delete, fn sweep_delete_pkg_playerbots_goal(ctx, character_guid) { From a6c0a67a56af817af6f67c4894a781d6a7aec6b9 Mon Sep 17 00:00:00 2001 From: T3 Code Date: Tue, 8 Sep 2026 12:31:11 +0000 Subject: [PATCH 2/5] fix(playerbots): pin the durable fixture toolchain and core revision --- .github/workflows/core-tip.yml | 7 ++++--- playerbots/src/goals.rs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/core-tip.yml b/.github/workflows/core-tip.yml index 3a4febe..1db2359 100644 --- a/.github/workflows/core-tip.yml +++ b/.github/workflows/core-tip.yml @@ -42,7 +42,7 @@ jobs: with: repository: LyraCoreProject/LyraCore # Restore main after the owning core operations merge. - ref: 5322de73ab06e557618a96b2f6609a9150b9c352 + ref: 257abc5d7d24bb7789e62c54838ab117d28d3fb3 path: lyracore - name: Install Rust 1.93.0 @@ -67,7 +67,7 @@ jobs: with: repository: LyraCoreProject/LyraCore # Restore main after the owning core operations merge. - ref: 5322de73ab06e557618a96b2f6609a9150b9c352 + ref: 257abc5d7d24bb7789e62c54838ab117d28d3fb3 path: lyracore # `spacetime generate` builds the module to wasm and extracts its schema through it, so this @@ -113,6 +113,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 env: + RUSTUP_TOOLCHAIN: 1.93.0 SPACETIME_VERSION: 2.7.1 SPACETIME_ARCHIVE_SHA256: 9b58192135733083479f30aba47dbbbc79b51db368c46cc221baa663e9c3a2c1 steps: @@ -126,7 +127,7 @@ jobs: with: repository: LyraCoreProject/LyraCore # Restore main after the owning core operations merge. - ref: 5322de73ab06e557618a96b2f6609a9150b9c352 + ref: 257abc5d7d24bb7789e62c54838ab117d28d3fb3 path: lyracore persist-credentials: false - name: Install the Rust toolchain and Wasm target diff --git a/playerbots/src/goals.rs b/playerbots/src/goals.rs index 414e396..7e1a6ba 100644 --- a/playerbots/src/goals.rs +++ b/playerbots/src/goals.rs @@ -1532,7 +1532,7 @@ fn quest_log(ctx: &ReducerContext, character_guid: u64) -> Vec Date: Tue, 8 Sep 2026 12:36:42 +0000 Subject: [PATCH 3/5] ci(playerbots): verify reviewed core head --- .github/workflows/core-tip.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/core-tip.yml b/.github/workflows/core-tip.yml index 1db2359..127afa7 100644 --- a/.github/workflows/core-tip.yml +++ b/.github/workflows/core-tip.yml @@ -42,7 +42,7 @@ jobs: with: repository: LyraCoreProject/LyraCore # Restore main after the owning core operations merge. - ref: 257abc5d7d24bb7789e62c54838ab117d28d3fb3 + ref: a2834d88e75af38bf6935efc7cca35a9e0fb631d path: lyracore - name: Install Rust 1.93.0 @@ -67,7 +67,7 @@ jobs: with: repository: LyraCoreProject/LyraCore # Restore main after the owning core operations merge. - ref: 257abc5d7d24bb7789e62c54838ab117d28d3fb3 + ref: a2834d88e75af38bf6935efc7cca35a9e0fb631d path: lyracore # `spacetime generate` builds the module to wasm and extracts its schema through it, so this @@ -127,7 +127,7 @@ jobs: with: repository: LyraCoreProject/LyraCore # Restore main after the owning core operations merge. - ref: 257abc5d7d24bb7789e62c54838ab117d28d3fb3 + ref: a2834d88e75af38bf6935efc7cca35a9e0fb631d path: lyracore persist-credentials: false - name: Install the Rust toolchain and Wasm target From c97464f6be211525a3b36a2ee3824cf259edbdd1 Mon Sep 17 00:00:00 2001 From: T3 Code Date: Tue, 8 Sep 2026 12:56:42 +0000 Subject: [PATCH 4/5] fix(playerbots): reset fixture-owned interaction rows --- .github/workflows/core-tip.yml | 6 +++--- playerbots/src/fixture.rs | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/workflows/core-tip.yml b/.github/workflows/core-tip.yml index 127afa7..54ff66e 100644 --- a/.github/workflows/core-tip.yml +++ b/.github/workflows/core-tip.yml @@ -42,7 +42,7 @@ jobs: with: repository: LyraCoreProject/LyraCore # Restore main after the owning core operations merge. - ref: a2834d88e75af38bf6935efc7cca35a9e0fb631d + ref: fe9caef6eb0d132c45a5d151827ab6fe64925f79 path: lyracore - name: Install Rust 1.93.0 @@ -67,7 +67,7 @@ jobs: with: repository: LyraCoreProject/LyraCore # Restore main after the owning core operations merge. - ref: a2834d88e75af38bf6935efc7cca35a9e0fb631d + ref: fe9caef6eb0d132c45a5d151827ab6fe64925f79 path: lyracore # `spacetime generate` builds the module to wasm and extracts its schema through it, so this @@ -127,7 +127,7 @@ jobs: with: repository: LyraCoreProject/LyraCore # Restore main after the owning core operations merge. - ref: a2834d88e75af38bf6935efc7cca35a9e0fb631d + ref: fe9caef6eb0d132c45a5d151827ab6fe64925f79 path: lyracore persist-credentials: false - name: Install the Rust toolchain and Wasm target diff --git a/playerbots/src/fixture.rs b/playerbots/src/fixture.rs index 1e22565..01a869e 100644 --- a/playerbots/src/fixture.rs +++ b/playerbots/src/fixture.rs @@ -164,6 +164,7 @@ pub fn playerbots_fixture_blocked_quest(ctx: &ReducerContext, guid: u64) -> Resu }; ctx.db.game_creature_spawn().guid().delete(target_guid); let spawn = ctx.db.game_creature_spawn().insert(spawn); + crate::creatures::despawn_creature_entity(ctx, target_guid); crate::creatures::insert_creature_entity( ctx, crate::creatures::build_creature_entity(&spawn, &template, 0, 0), @@ -403,6 +404,22 @@ pub fn playerbots_fixture_interaction_stage( template.src_item_count = 1; ctx.db.game_quest_template().entry().delete(QUEST); ctx.db.game_quest_template().insert(template); + for row in ctx + .db + .game_creature_quest() + .by_creature() + .filter(5_090_101u32) + .filter(|row| row.quest_entry == QUEST) + .collect::>() + { + ctx.db.game_creature_quest().id().delete(row.id); + } + ctx.db.game_quest_objective().id().delete(COLLECT as u64); + ctx.db + .game_quest_reward_item() + .id() + .delete(GUARANTEED as u64); + ctx.db.game_quest_reward_choice().id().delete(CHOICE as u64); ctx.db.game_quest_objective().insert(crate::QuestObjective { id: COLLECT as u64, quest_entry: QUEST, From 0781d7e5c8831fd3cec9235bfbdb84dc2cd0537c Mon Sep 17 00:00:00 2001 From: T3 Code Date: Tue, 8 Sep 2026 13:14:56 +0000 Subject: [PATCH 5/5] ci(playerbots): verify merged core main --- .github/workflows/core-tip.yml | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/workflows/core-tip.yml b/.github/workflows/core-tip.yml index 54ff66e..cb93caa 100644 --- a/.github/workflows/core-tip.yml +++ b/.github/workflows/core-tip.yml @@ -37,12 +37,11 @@ jobs: with: path: collection - - name: Check out the required LyraCore revision + - name: Check out LyraCore main uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: LyraCoreProject/LyraCore - # Restore main after the owning core operations merge. - ref: fe9caef6eb0d132c45a5d151827ab6fe64925f79 + ref: main path: lyracore - name: Install Rust 1.93.0 @@ -62,12 +61,11 @@ jobs: with: path: collection - - name: Check out the required LyraCore revision + - name: Check out LyraCore main uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: LyraCoreProject/LyraCore - # Restore main after the owning core operations merge. - ref: fe9caef6eb0d132c45a5d151827ab6fe64925f79 + ref: main path: lyracore # `spacetime generate` builds the module to wasm and extracts its schema through it, so this @@ -122,12 +120,11 @@ jobs: with: path: collection persist-credentials: false - - name: Check out the required LyraCore revision + - name: Check out LyraCore main uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: LyraCoreProject/LyraCore - # Restore main after the owning core operations merge. - ref: fe9caef6eb0d132c45a5d151827ab6fe64925f79 + ref: main path: lyracore persist-credentials: false - name: Install the Rust toolchain and Wasm target