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
31 changes: 31 additions & 0 deletions dungeons/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,34 @@ module, so keep item names dungeon-prefixed.

Entries and positions are dump-verified against classic-db z2815. Omitted: Smite's stomp stun (no
vanilla data row backs it).

Door and encounter rows persist in the Module across a Standalone restart. A restart preserves
open doors for completed encounters and closed doors for unfinished encounters.

## Verification

Install this Package in the Core checkout, then run its durable test:

```sh
python3 dungeons/testsuite/run.py /path/to/LyraCore test --locked \
--test deadmines_doors -- --ignored --test-threads=1
```

The test uses a private persistent Standalone. It kills Rhahk'Zor and the Shredder, fires the
Defias Cannon through the production use path, and restarts the node. The Factory and Iron Clad
Doors must remain open; Sneed's and Gilnid's doors must remain closed. It then kills Sneed and
Gilnid and restarts again. All four doors and their completed encounter states must persist.
Core supplies the private fixture through `lyracore-test-support`; the runner selects Core and
keeps build output outside the Package.

The stage reducers in `src/deadmines_verify.rs` take no arguments because fixture GUIDs exceed
the CLI's safe integer range. After setup, each stage resolves its subjects in the Module, calls
the production kill, damage, or gameobject use path, and checks the durable outcome in the same
transaction.
Core's `module/tests/deadmines_package.rs` also calls these stages to check the boss deaths,
cannon, and Smite's weapon changes:

```sh
cd /path/to/LyraCore
cargo test -p lyracore-module --test deadmines_package -- --ignored --test-threads=1
```
23 changes: 4 additions & 19 deletions dungeons/src/deadmines.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,4 @@
//! Deadmines (map 36) choreography — the encounter kernel's first consumer.
//!
//! Unlike the retired EventAI-routed dungeons, the vanilla data carries NO
//! `ACTION_T_SET_INSTANCE_DATA` rows for any Deadmines creature (dump-verified against classic-db
//! z2815: their ACID rows are combat spells and yells only, which import generically — VanCleef's
//! yells and his 50%-HP allies summon need nothing from this Package). The door, cannon, and
//! Mr. Smite behavior lived in a C++ instance script upstream, so this Package rebuilds it on the
//! kernel's notify hooks and primitives:
//!
//! - `on_creature_death` — Rhahk'Zor, Sneed, and Gilnid each open their door; Sneed's Shredder
//! ejects Sneed on destruction.
//! - `on_go_used` — firing the Defias Cannon breaches the Iron Clad Door.
//! - `on_hp_threshold` + `equip_swap` + `move_to_point` — Mr. Smite's stair scene: at 66% he
//! runs to his chest and comes back dual-wielding, at 33% he switches to his two-hand hammer.
//! His yells are this Package's own content; the source data has no text rows for them.
//!
//! Omitted from the scene: Smite's stomp stun (no vanilla data row backs it; revisit if a
//! curated spell mapping lands).
//! Read dungeons/README.md#deadmines before changing encounter behavior.

use spacetimedb::ReducerContext;

Expand Down Expand Up @@ -187,7 +170,9 @@ fn eject_sneed(ctx: &ReducerContext, shredder_guid: u64, instance_id: u64) {

fn open_door_logged(ctx: &ReducerContext, go_entry: u32, instance_id: u64) {
if let Err(error) = encounter::open_door(ctx, go_entry, instance_id) {
spacetimedb::log::warn!("deadmines: door {go_entry} did not open: {error}");
spacetimedb::log::warn!(
"deadmines: door {go_entry} in instance {instance_id} did not open: {error}"
);
}
}

Expand Down
54 changes: 20 additions & 34 deletions dungeons/src/deadmines_verify.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
#![cfg(feature = "debug_reducers")]

//! Deadmines verification harness (`debug_reducers` only): stage reducers the durable test
//! (`module/tests/deadmines_package.rs`) calls in order over the standalone CLI. Every reducer is
//! argument-free — fixture guids exceed the CLI's safe integer range, so each stage resolves its
//! own subjects server-side, drives the REAL production path (kill, damage, gameobject use), and
//! verifies the durable outcome in the same call.
//! Read dungeons/README.md#verification before running these fixture reducers.

use spacetimedb::{reducer, ReducerContext, Table};

Expand All @@ -25,10 +21,10 @@ const FIXTURE_LOW_BAND: u64 = 0x10_0000;
const DEADMINES_INSTANCE: u64 = 936;
const GO_FIXTURE_BAND: u64 = 0xF11D << 48;

/// Seed a Deadmines fixture instance: doors, cannon, chest, Smite's weapons, the bosses, and one
/// player. Positions come from the classic-db z2815 spawn rows.
/// Requires a Shard without imported content; positions follow classic-db z2815.
#[reducer]
pub fn debug_deadmines_begin(ctx: &ReducerContext) -> Result<(), String> {
crate::package::fixture::require_no_imported_content(ctx)?;
install_instance(ctx, MAP_ID, DEADMINES_INSTANCE)?;
for (entry, type_id, display_id, x, y, z, orientation) in [
(
Expand All @@ -54,7 +50,7 @@ pub fn debug_deadmines_begin(ctx: &ReducerContext) -> Result<(), String> {
),
(SMITES_CHEST, 5, 259, 2.69086, -781.633, 9.76985, 2.33874),
] {
seed_gameobject(ctx, entry, type_id, display_id, x, y, z, orientation)?;
seed_gameobject(ctx, entry, type_id, display_id, [x, y, z, orientation])?;
}
seed_smite_weapon(ctx, SMITES_REAVER, "Smite's Reaver", 13913)?;
seed_smite_weapon(ctx, SMITES_MIGHTY_HAMMER, "Smite's Mighty Hammer", 19610)?;
Expand All @@ -71,8 +67,7 @@ pub fn debug_deadmines_begin(ctx: &ReducerContext) -> Result<(), String> {
let player = spawn_fixture_player(ctx, MAP_ID, DEADMINES_INSTANCE, 44)?;
set_fixture_position(ctx, player, -5.0, -760.0, 9.8)?;

// The real fixture registers these through the Package's `on_creature_spawn` hook when Smite
// spawns via the production path; `spawn_source` writes the row directly, so arm them here.
// Direct fixture inserts do not fire on_creature_spawn.
encounter::watch_hp_threshold(ctx, MR_SMITE, SMITE_FIRST_STAND_PCT)?;
encounter::watch_hp_threshold(ctx, MR_SMITE, SMITE_SECOND_STAND_PCT)?;
Ok(())
Expand Down Expand Up @@ -146,12 +141,11 @@ pub fn debug_deadmines_gilnid_falls(ctx: &ReducerContext) -> Result<(), String>
require_done(ctx, ENCOUNTER_GILNID, "Gilnid encounter state is not Done")
}

/// Damage Smite through his 66% threshold: yell, chest run, dual Reavers.
#[reducer]
pub fn debug_deadmines_smite_improvises(ctx: &ReducerContext) -> Result<(), String> {
damage_smite_to_pct(ctx, 60)?;
require_yell_containing(ctx, "improvise")?;
require_smite_at_chest(ctx)?;
require_smite_chest_run(ctx)?;
require_smite_equip(ctx, SMITES_REAVER, SMITES_REAVER, "66%")
}

Expand Down Expand Up @@ -189,20 +183,14 @@ pub fn debug_deadmines_smite_falls(ctx: &ReducerContext) -> Result<(), String> {
)
}

// -------------------------------------------------------------------------------------------
// Fixture plumbing
// -------------------------------------------------------------------------------------------

fn seed_gameobject(
ctx: &ReducerContext,
entry: u32,
type_id: u8,
display_id: u32,
x: f32,
y: f32,
z: f32,
orientation: f32,
position: [f32; 4],
) -> Result<(), String> {
let [x, y, z, orientation] = position;
let templates = ctx.db.game_gameobject_template();
if templates.entry().find(entry).is_none() {
templates.insert(crate::tables::GameObjectTemplate {
Expand Down Expand Up @@ -342,31 +330,29 @@ fn require_yell_containing(ctx: &ReducerContext, fragment: &str) -> Result<(), S
)
}

fn require_smite_at_chest(ctx: &ReducerContext) -> Result<(), String> {
fn require_smite_chest_run(ctx: &ReducerContext) -> Result<(), String> {
let smite_guid = fixture_guid(MR_SMITE, 43);
let smite = ctx
let leg = ctx
.db
.game_world_entity()
.game_creature_spline()
.guid()
.find(smite_guid)
.ok_or_else(|| "Mr. Smite disappeared".to_string())?;
.ok_or_else(|| "Smite's chest run emitted no movement spline".to_string())?;
let chest = ctx
.db
.game_gameobject()
.guid()
.find(GO_FIXTURE_BAND | u64::from(SMITES_CHEST))
.ok_or_else(|| "Smite's Chest disappeared".to_string())?;
// Movement updates the stored position on later ticks.
require(
(smite.x - chest.x).abs() < 0.5 && (smite.y - chest.y).abs() < 0.5,
"Smite did not run to his chest",
)?;
require(
ctx.db
.game_creature_spline()
.guid()
.find(smite_guid)
.is_some(),
"Smite's chest run emitted no movement spline",
leg.map_id == chest.map_id
&& leg.instance_id == chest.instance_id
&& leg.run
&& (leg.dx - chest.x).abs() < 0.5
&& (leg.dy - chest.y).abs() < 0.5
&& (leg.dz - chest.z).abs() < 0.5,
"Smite's run does not lead to his chest",
)
}

Expand Down
128 changes: 128 additions & 0 deletions dungeons/testsuite/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions dungeons/testsuite/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "lyracore-dungeons-tests"
version = "0.1.0"
edition = "2021"
rust-version = "1.93.0"
publish = false

[workspace]

[dev-dependencies]
lyracore-test-support = "0.1.0"
38 changes: 38 additions & 0 deletions dungeons/testsuite/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
"""Read dungeons/README.md#verification before running against a Core checkout."""

import json
import os
from pathlib import Path
import subprocess
import sys
import tomllib


def main() -> int:
if len(sys.argv) < 3:
raise SystemExit("usage: run.py CORE COMMAND [CARGO-ARGS...]")
core = Path(sys.argv[1]).resolve(strict=True)
command, *arguments = sys.argv[2:]
suite = Path(__file__).resolve().parent
installed = core / "packages/dungeons"
if not installed.is_dir() or installed.resolve() != suite.parent:
raise SystemExit("install this dungeons Package in the selected Core checkout first")
if command not in {"test", "check", "clippy", "fmt", "generate-lockfile"}:
raise SystemExit(f"unsupported Cargo command: {command}")
toolchain = tomllib.loads((core / "rust-toolchain.toml").read_text())["toolchain"]["channel"]
support = core / "crates/lyracore-test-support"
if not (support / "Cargo.toml").is_file():
raise SystemExit(f"Core does not provide lyracore-test-support: {support}")
environment = os.environ.copy()
environment["LYRACORE_TEST_CORE"] = str(core)
environment.setdefault("CARGO_TARGET_DIR", str(core / "target"))
patch = ["--config", f"patch.crates-io.lyracore-test-support.path={json.dumps(str(support))}"]
cargo = ["cargo", f"+{toolchain}"]
cargo += [*patch, command] if command == "fmt" else [command, *patch]
cargo += ["--manifest-path", str(suite / "Cargo.toml"), *arguments]
return subprocess.run(cargo, cwd=core, env=environment).returncode


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading