Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/core-tip.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ jobs:
steps:
- name: Check out Packages
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Test and enforce the no-committed-Package-Delta firewall
run: |
Expand All @@ -36,13 +38,15 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: collection
persist-credentials: false

- name: Check out LyraCore main
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: LyraCoreProject/LyraCore
ref: main
path: lyracore
persist-credentials: false

- name: Install Rust 1.93.0
run: rustup toolchain install 1.93.0 --profile minimal
Expand All @@ -60,13 +64,15 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: collection
persist-credentials: false

- name: Check out LyraCore main
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: LyraCoreProject/LyraCore
ref: main
path: lyracore
persist-credentials: false

# `spacetime generate` builds the module to wasm and extracts its schema through it, so this
# job needs the same toolchain + target the `module` job's compile check needs, plus the
Expand Down Expand Up @@ -146,6 +152,33 @@ jobs:
tar --extract --gzip --file "$archive_path" --directory "$cli_dir"
ln -s spacetimedb-cli "$cli_dir/spacetime"
echo "$cli_dir" >> "$GITHUB_PATH"
- name: Check out the preceding Module source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: LyraCoreProject/LyraCore
ref: be3fa67d0f0c24749230560544a3e8e8b577f61d
path: preceding-core
persist-credentials: false
- name: Check out the preceding Package source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: LyraCoreProject/packages
ref: 5724de1660a2628e88320914bdd5abb0c69da517
path: preceding-collection
persist-credentials: false
- name: Build the pinned preceding Wasm for populated migration
working-directory: preceding-core
env:
CARGO_BUILD_JOBS: 2
CARGO_INCREMENTAL: 0
run: |
export CARGO_TARGET_DIR="$GITHUB_WORKSPACE/preceding-core/target"
cargo clean --manifest-path "$GITHUB_WORKSPACE/preceding-core/Cargo.toml" -p lyracore-module
"$GITHUB_WORKSPACE/preceding-collection/.github/check-core-tip.sh" "$GITHUB_WORKSPACE/preceding-core" \
cargo build --manifest-path "$GITHUB_WORKSPACE/preceding-core/Cargo.toml" \
--locked --release --target wasm32-unknown-unknown -p lyracore-module --features=debug_reducers
cp "$CARGO_TARGET_DIR/wasm32-unknown-unknown/release/lyracore_module.wasm" "$RUNNER_TEMP/playerbots-preceding.wasm"
echo "PLAYERBOTS_PRECEDING_WASM=$RUNNER_TEMP/playerbots-preceding.wasm" >> "$GITHUB_ENV"
- name: Run the real Module and Package action fixture
run: |
cargo clean --manifest-path "$GITHUB_WORKSPACE/lyracore/Cargo.toml" -p lyracore-module
Expand Down
77 changes: 77 additions & 0 deletions playerbots/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,3 +397,80 @@ storage and reward exchanges, including bank holdings and other rewards in the s
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.

## Durable controller and objective

`playerbots_select_controller(guid, controller)` selects one controller for that Character. The
SpacetimeDB argument names are `legacy`, `recordOnly`, `cohort`, and `frozen`.

| Controller | Behavior |
| --- | --- |
| Legacy | Existing quest and party policy. This is the additive migration default. |
| RecordOnly | Records the new decision without issuing gameplay from either controller. |
| Cohort | Runs the durable objective and typed action runner. |
| Frozen | Cancels Package casts and movement, stops bot attacks, and prevents new bot work. |

Selection also updates core-owned session-less action consent. Legacy and Cohort allow group
admission; RecordOnly and Frozen suppress it. Every selection clears this Character's unclaimed
Group Intents, including a repeated selection. A group action admitted before the selection may
finish at Realm-core afterwards. There is no atomic operation across Shards.

Every gameplay entry checks current Account Claim and Fence ownership and Character World Session
status. A human taking ownership suspends the bot even before `online` changes. Cancellation matches
the Package cast identity or movement start time, so it preserves a human's later work. Bot control
can resume after ownership ends. The ownership Gate permits an absent body for Legacy restoration;
group admission additionally requires a live entity.

The Cohort behavior in this revision returns to the roster's home point. It retains that objective
while healing itself with a known supported rotation spell, defending against damage, or fleeing
at its configured health threshold. Companion orders and quest catalog execution remain separate
work. The existing Legacy policy remains available under its explicit selector.

Recovery scans at most 24 indexed healing rotation rows and revalidates at most two retained
rows per pass. The separate
`pkg_playerbots_recovery_scan` explanation records its indexed cursor, scanned row count,
and Pending or Complete stage. A first incomplete scan selects Recovery Hold. A completed missing-spell result keeps lower-priority
actions eligible while later scans continue. Completed
scans restart on the next pass, so inserted or edited rows are reconsidered. A retained
spell is checked against its current class, role, condition, and exact learned-spell row
before use. Defense in this controller responds to the live attacker recorded by incoming
damage; target acquisition and assistance remain downstream work.

The normal Package tick reads the `(next_think_micros, id)` index and processes at most 16 due bots.
Excess bots keep their due time. They precede bots whose turn already advanced the clock. The
scheduler row reports the processed identities and the oldest deferred lag. Runner rows backfill
only for that bounded batch, or for one explicitly selected bot.

`pkg_playerbots_runner` is the explanation read. It reports the retained objective and destination,
chosen target and reason, ordered candidates, foreground cast or movement, latest outcome, progress
age at `observed_micros`, retry count, next eligible time, and catalog revision. It retains eight
meaningful transitions, four failures and four deferred destinations per Character. Movement,
combat, cast and quest evidence have separate clocks. An accepted attack does not advance any of
them. A completed cast records dispatched effects, and does not complete the return-home objective.

One foreground action retains the controller generation and partition. A higher-priority action
cancels incompatible work before starting. Pending casts retain the core scheduled identity and
refresh its current due time after direct-damage pushback. After a movement request, arrival requires a later position
observation. Ten seconds without movement records a failure; three failed intervals defer the
same destination for 30 seconds. Deferred keys include map, instance and optional navigation
coverage generation. A coverage change invalidates the prior destination decision.

The selector allows at most 24 candidates, depth four, 16 transitions and one route request with
4096 expansions per decision. These limits also apply to prerequisites, alternatives and continuers.
Strategies use typed triggers and integer priority adjustments. Each Candidate carries a typed action
payload, reason and objective identity. Movement names Home or an Entity; Cast carries its spell and
target; Attack carries its target. There is no string registry.

The migration appends `controller = Legacy` and `scheduler_lag_micros = 0` to the roster and adds
runner, recovery scan and scheduler tables. Existing goals and action observations keep their schema and meaning.
The roster selector travels with the Character; runner observations and foreground work do not.
The Character delete operation removes the runner and recovery scan rows. Production publication still requires the
schema review described in LyraCore's `docs/danger-zones.md`.

The durable fixture now includes populated migration. Before running the complete ignored target,
build Module Wasm from core `be3fa67d0f0c24749230560544a3e8e8b577f61d` with collection
`5724de1660a2628e88320914bdd5abb0c69da517`, retain an immutable copy, and set
`PLAYERBOTS_PRECEDING_WASM` to that path. Package CI performs this build automatically. The case
publishes that preceding Wasm, populates real bot, goal, quest and action rows, then upgrades the
same private Standalone to the current Wasm. It records both Wasm identities and asserts the old
rows survive, selector defaults apply, and backfill takes bounded passes.
12 changes: 8 additions & 4 deletions playerbots/src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ crate::character_owned!(delete, fn sweep_delete_pkg_playerbots_action(ctx, chara
});
crate::character_owned!(not_transported, fn sweep_transfer_pkg_playerbots_action());

fn prior(ctx: &ReducerContext, guid: u64, kind: ActionKind) -> Option<PlayerbotsAction> {
pub(super) fn observation(
ctx: &ReducerContext,
guid: u64,
kind: ActionKind,
) -> Option<PlayerbotsAction> {
ctx.db
.pkg_playerbots_action()
.by_character()
Expand All @@ -61,7 +65,7 @@ fn prior(ctx: &ReducerContext, guid: u64, kind: ActionKind) -> Option<Playerbots

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) {
if let Some(before) = observation(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;
Expand Down Expand Up @@ -143,7 +147,7 @@ pub(super) fn cast(
}

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; };
let Some(mut row) = observation(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,
Expand Down Expand Up @@ -241,7 +245,7 @@ pub(super) fn movement(
) {
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 Some(before) = observation(ctx, guid, ActionKind::Move) {
if let ActionOutcome::Movement(previous) = before.outcome {
if previous.map_id == map_id
&& previous.instance_id == instance_id
Expand Down
Loading
Loading