Skip to content
Open
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
90 changes: 90 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,96 @@ Current useful baselines from recent handoff:

If a test fails, do not assume production is wrong or the test is wrong. Contrast with C++ and document which one it is.

## QA Bot / Client Automation

The live client QA bot is an integrated project QA tool, not throwaway scratch code or a separate
side project.

- Integrated bot path: `tools/wow-test-bot`
- It is intentionally excluded from the root Cargo workspace so its live-QA dependencies and local
runtime assumptions do not affect normal `cargo check/test` or CI.
- Temporary experiments may use a `/tmp/...` copy when sandbox permissions require it, but useful
bot improvements must be ported back to `tools/wow-test-bot` and committed with the RustyCore PR
before the work is considered preserved.
- Keep server fixes and bot-tooling changes logically separated in the commit message/body when
practical. Mention the bot scenario and report path in issue/PR QA notes when a PR depends on a
new bot capability.

Baseline login smoke:

```bash
cd /home/server/rustycore/tools/wow-test-bot
WOW_BOT_PASSWORD='local-password' ./run_rustycore_login_smoke.sh
```

Useful overrides:

```bash
WOW_BOT_PASSWORD='local-password' WOW_BOT_ACCOUNT=TESTBOT5@bot.local ./run_rustycore_login_smoke.sh
WOW_BOT_PASSWORD='local-password' WOW_BOT_REPORT=/tmp/rustycore-bot-report.json WOW_BOT_LOG=/tmp/rustycore-bot.log ./run_rustycore_login_smoke.sh
WOW_BOT_PASSWORD='local-password' BNET_HOST=127.0.0.1 BNET_PORT=8081 WORLD_HOST=127.0.0.1 WORLD_PORT=8085 REALM_ID=1 ./run_rustycore_login_smoke.sh
```

Quest/gossip smoke for QA of one questgiver:

```bash
WOW_BOT_QUEST_SMOKE=1 \
WOW_BOT_QUEST_CREATURE_ENTRY=<creature-template-entry> \
WOW_BOT_QUEST_EXPECT_ID=<quest-id> \
WOW_BOT_PASSWORD='local-password' \
./run_rustycore_login_smoke.sh
```

Optional quest overrides include `WOW_BOT_QUEST_CREATURE_GUID` for an exact
`world.creature.guid`, `WOW_BOT_QUEST_MAP_ID`, `WOW_BOT_QUEST_FORBID_ID`,
`WOW_BOT_QUEST_FORBID_TITLE_CONTAINS`, `WOW_BOT_QUEST_QUERY_DETAILS=0`,
`WOW_BOT_QUEST_RESET=1`, `WOW_BOT_QUEST_RELOCATE=1`,
`WOW_BOT_QUEST_RUNTIME_COUNTER=<visible-counter>`, `WOW_BOT_QUEST_SET_RACE=<id>`,
`WOW_BOT_QUEST_SET_CLASS=<id>`, `WOW_BOT_QUEST_SET_LEVEL=<1-80>`, and
`WOW_BOT_QUEST_ACCEPT=1`.

For deterministic quest accept QA, prefer a fully specified flow that prepares the selected test
character before login, for example:

```bash
WOW_BOT_QUEST_SMOKE=1 \
WOW_BOT_QUEST_CREATURE_ENTRY=15278 \
WOW_BOT_QUEST_RUNTIME_COUNTER=90 \
WOW_BOT_QUEST_MAP_ID=530 \
WOW_BOT_QUEST_EXPECT_ID=9393 \
WOW_BOT_QUEST_RESET=1 \
WOW_BOT_QUEST_RELOCATE=1 \
WOW_BOT_QUEST_SET_RACE=10 \
WOW_BOT_QUEST_SET_CLASS=3 \
WOW_BOT_QUEST_SET_LEVEL=3 \
WOW_BOT_QUEST_ACCEPT=1 \
WOW_BOT_PASSWORD='local-password' \
./run_rustycore_login_smoke.sh
```

The bot authenticates against the live `bnet-server`, writes/uses auth DB session data, connects to
`world-server`, enumerates characters, and enters world. It requires the local runtime and MariaDB
databases (`auth`, `characters`, `world`, `hotfixes`) to be available. Do not print, stage, or
commit bot credentials, local configs, DB URLs with secrets, generated logs containing secrets, or
runtime certificates. `tools/wow-test-bot/config.example.json` is versioned with blank passwords;
use `WOW_BOT_PASSWORD`, `WOW_BOT_PASSWORD_<ACCOUNT>`, or an ignored local `config.json`.
`tools/wow-test-bot/.env.local` is also ignored and may be used for local-only passwords or DB URL
overrides. If DB URL env vars are omitted, the bot reads database connection info from
`WOW_BOT_DB_CONF` (default `/home/server/trinity-legacy-install/etc/worldserver.conf`).

When extending the bot for an issue:

1. Anchor packet layouts/opcodes to C++ source or to a real capture before sending new packets.
2. Add a focused CLI mode or wrapper script for the QA scenario, not a one-off manual edit.
3. Report pass/fail as structured JSON fields so PR QA can cite the exact checks.
4. Keep the first version narrow: login, one interaction, one expected server response, then expand.
5. Commit useful bot work under `tools/wow-test-bot`; do not leave the only copy in `/tmp`.

Runtime bot QA complements, but does not replace, `cargo` validation and CI. A PR is not
`manual-test-ready` merely because bot code exists; install/restart the target server build, run the
bot scenario, and record the result in the PR/issue. Until CI has a full live server + DB fixture,
bot runs are local QA evidence rather than required GitHub status checks.

## Architecture: Current Runtime Reality

The runtime currently has three coexisting world models. This is important; old notes that describe a single pending `MapManager` integration are stale.
Expand Down
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ members = [
# Tooling
"crates/capture-diff",
]
exclude = [
"tools/wow-test-bot",
]

[workspace.package]
version = "0.1.0"
Expand Down
44 changes: 26 additions & 18 deletions crates/world-server/src/creature_loaded_grid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1991,7 +1991,7 @@ mod tests {
[selection(entry)],
);

let map_object_guid = map_creature_guid(entry, 571, 99_001);
let map_object_guid = map_creature_guid(entry, 571, 55);
let resolved = resolver
.resolve_loaded_grid_creature_like_cpp(55, map_object_guid)
.expect("resolver should build lifecycle record");
Expand Down Expand Up @@ -2117,7 +2117,7 @@ mod tests {
);

let resolved = resolver
.resolve_loaded_grid_creature_like_cpp(55, map_creature_guid(entry, 571, 99_001))
.resolve_loaded_grid_creature_like_cpp(55, map_creature_guid(entry, 571, 55))
.expect("resolver should build lifecycle record");

assert_eq!(resolved.creature.sparring_health_pct(), 35.5);
Expand Down Expand Up @@ -2162,7 +2162,10 @@ mod tests {
);

let resolved = resolver
.resolve_loaded_grid_creature_like_cpp(spawn_id, map_creature_guid(entry, 571, 99_002))
.resolve_loaded_grid_creature_like_cpp(
spawn_id,
map_creature_guid(entry, 571, spawn_id as i64),
)
.expect("resolver should build lifecycle record");

assert_eq!(
Expand Down Expand Up @@ -2273,7 +2276,8 @@ mod tests {
}

#[test]
fn loaded_grid_creature_lifecycle_resolver_uses_caller_map_guid_not_spawn_id_low() {
fn loaded_grid_creature_lifecycle_resolver_accepts_map_owned_low_guid_distinct_from_spawn_id_like_cpp()
{
let entry = 12_349;
let spawn_id = 61;
let caller_low_guid = 345_678;
Expand All @@ -2284,19 +2288,17 @@ mod tests {
[selection(entry)],
);

assert_ne!(spawn_id as i64, caller_low_guid);
let resolved = resolver
.resolve_loaded_grid_creature_like_cpp(spawn_id, map_object_guid)
.expect("caller-owned map guid should be preserved");

assert_ne!(spawn_id as i64, caller_low_guid);
assert_eq!(resolved.lifecycle_record.create.guid, map_object_guid);
assert_eq!(resolved.creature.guid(), map_object_guid);
let recorded = resolved
.expect("C++ Creature::LoadFromDB uses map-owned lowguid, not spawn id, for live GUID");
let creature = resolved
.map_object_record
.as_ref()
.and_then(MapObjectRecord::creature)
.expect("map insertion record should contain the created creature");
assert_eq!(recorded.guid(), map_object_guid);
.and_then(|record| record.creature())
.expect("resolver should build a creature record");
assert_eq!(creature.guid(), map_object_guid);
assert_eq!(creature.lifecycle_metadata().spawn_id, spawn_id);
}

#[test]
Expand All @@ -2318,7 +2320,10 @@ mod tests {
[selection(entry)],
);
let resolved = resolver
.resolve_loaded_grid_creature_like_cpp(spawn_id, map_creature_guid(entry, 571, 99_062))
.resolve_loaded_grid_creature_like_cpp(
spawn_id,
map_creature_guid(entry, 571, spawn_id as i64),
)
.expect("formation metadata should not block loaded-grid resolver");

assert_eq!(
Expand All @@ -2345,7 +2350,10 @@ mod tests {
[selection(entry)],
);
let resolved = resolver
.resolve_loaded_grid_creature_like_cpp(spawn_id, map_creature_guid(entry, 571, 99_063))
.resolve_loaded_grid_creature_like_cpp(
spawn_id,
map_creature_guid(entry, 571, spawn_id as i64),
)
.expect("absence of formation metadata is the previous behavior");

assert!(resolved.creature.formation_info_like_cpp().is_none());
Expand All @@ -2368,7 +2376,7 @@ mod tests {
[selection(entry)],
);

let map_object_guid = map_creature_guid(entry, 571, 99_002);
let map_object_guid = map_creature_guid(entry, 571, 56);
let resolved = resolver
.resolve_loaded_grid_creature_like_cpp(56, map_object_guid)
.expect("resolver should build creature without insertion request");
Expand Down Expand Up @@ -2482,7 +2490,7 @@ mod tests {
fn loaded_grid_creature_lifecycle_resolver_accepts_vehicle_template_with_vehicle_guid_like_cpp()
{
let entry = 12_351;
let map_object_guid = map_vehicle_guid(entry, 571, 99_006);
let map_object_guid = map_vehicle_guid(entry, 571, 63);
let resolver = CreatureLoadedGridLifecycleResolverLikeCpp::new(
[vehicle_template(entry, 77)],
[spawn(63, entry, true)],
Expand Down Expand Up @@ -2562,7 +2570,7 @@ mod tests {
[spawn(60, entry, true)],
[selection(entry)],
);
let map_object_guid = map_creature_guid(entry, 571, 99_007);
let map_object_guid = map_creature_guid(entry, 571, 60);
let first = resolver
.resolve_loaded_grid_creature_like_cpp(60, map_object_guid)
.unwrap();
Expand Down
88 changes: 78 additions & 10 deletions crates/world-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20625,9 +20625,14 @@ mmap.enablePathFinding = 0

#[test]
fn loaded_grid_creature_respawn_record_variable_level_returns_creature_record_like_cpp() {
let mut metadata = test_spawn_metadata_with_flags([(67, 571, SpawnGroupFlags::NONE)]);
let spawn_id = 1;
let spawn_id = 54_984;
let entry = 42;
let mut metadata = test_spawn_metadata_with_explicit_spawn_ids([(
67,
571,
SpawnGroupFlags::NONE,
spawn_id,
)]);
metadata = metadata.with_creature_runtime_rows_like_cpp(BTreeMap::from([(
spawn_id,
super::spawn_store_loader::CreatureSpawnRuntimeRowLikeCpp {
Expand Down Expand Up @@ -20691,6 +20696,7 @@ mmap.enablePathFinding = 0
assert_eq!(u32::from(creature.guid().map_id()), 571);
assert_eq!(creature.guid().entry(), entry);
assert_eq!(creature.guid().counter(), 1);
assert_ne!(creature.guid().counter(), spawn_id as i64);
assert_eq!(creature.ai_max_health(), u64::from(level) * 20);
assert_eq!(creature.ai_current_health(), creature.ai_max_health());
}
Expand Down Expand Up @@ -21010,9 +21016,14 @@ mmap.enablePathFinding = 0

#[test]
fn loaded_grid_creature_spawn_group_spawn_record_does_not_require_respawn_timer_like_cpp() {
let mut metadata = test_spawn_metadata_with_flags([(68, 571, SpawnGroupFlags::NONE)]);
let spawn_id = 1;
let spawn_id = 54_985;
let entry = 42;
let mut metadata = test_spawn_metadata_with_explicit_spawn_ids([(
68,
571,
SpawnGroupFlags::NONE,
spawn_id,
)]);
metadata = metadata.with_creature_runtime_rows_like_cpp(BTreeMap::from([(
spawn_id,
super::spawn_store_loader::CreatureSpawnRuntimeRowLikeCpp {
Expand Down Expand Up @@ -21065,14 +21076,21 @@ mmap.enablePathFinding = 0
assert_eq!(creature.respawn_time(), 0);
assert_eq!(creature.lifecycle_metadata().spawn_id, spawn_id);
assert_eq!(creature.guid().entry(), entry);
assert_eq!(creature.guid().counter(), 1);
assert_ne!(creature.guid().counter(), spawn_id as i64);
}

#[test]
fn spawn_group_condition_update_spawn_loads_loaded_grid_creature_without_respawn_timer_like_cpp()
{
let mut metadata = test_spawn_metadata_with_flags([(69, 571, SpawnGroupFlags::NONE)]);
let spawn_id = 1;
let spawn_id = 54_986;
let entry = 42;
let mut metadata = test_spawn_metadata_with_explicit_spawn_ids([(
69,
571,
SpawnGroupFlags::NONE,
spawn_id,
)]);
metadata = metadata.with_creature_runtime_rows_like_cpp(BTreeMap::from([(
spawn_id,
super::spawn_store_loader::CreatureSpawnRuntimeRowLikeCpp {
Expand Down Expand Up @@ -21144,13 +21162,20 @@ mmap.enablePathFinding = 0
.expect("loaded-grid Creature should be indexed by spawn id");
assert_eq!(creature.respawn_time(), 0);
assert_eq!(creature.lifecycle_metadata().spawn_id, spawn_id);
assert_eq!(creature.guid().counter(), 1);
assert_ne!(creature.guid().counter(), spawn_id as i64);
}

#[test]
fn spawn_group_condition_update_tick_mirrors_loaded_grid_creature_to_legacy_like_cpp() {
let mut metadata = test_spawn_metadata_with_flags([(69, 571, SpawnGroupFlags::NONE)]);
let spawn_id = 1;
let spawn_id = 54_987;
let entry = 42;
let mut metadata = test_spawn_metadata_with_explicit_spawn_ids([(
69,
571,
SpawnGroupFlags::NONE,
spawn_id,
)]);
metadata = metadata.with_creature_runtime_rows_like_cpp(BTreeMap::from([(
spawn_id,
super::spawn_store_loader::CreatureSpawnRuntimeRowLikeCpp {
Expand Down Expand Up @@ -21216,6 +21241,8 @@ mmap.enablePathFinding = 0
.map()
.get_creature_by_spawn_id_like_cpp(spawn_id)
.expect("canonical loaded-grid creature");
assert_eq!(creature.guid().counter(), 1);
assert_ne!(creature.guid().counter(), spawn_id as i64);
assert!(
legacy
.read()
Expand All @@ -21229,9 +21256,14 @@ mmap.enablePathFinding = 0
#[test]
fn loaded_grid_creature_respawn_record_vehicle_template_uses_creature_low_vehicle_high_like_cpp()
{
let mut metadata = test_spawn_metadata_with_flags([(67, 571, SpawnGroupFlags::NONE)]);
let spawn_id = 1;
let spawn_id = 54_988;
let entry = 42;
let mut metadata = test_spawn_metadata_with_explicit_spawn_ids([(
67,
571,
SpawnGroupFlags::NONE,
spawn_id,
)]);
metadata = metadata.with_creature_runtime_rows_like_cpp(BTreeMap::from([(
spawn_id,
super::spawn_store_loader::CreatureSpawnRuntimeRowLikeCpp {
Expand Down Expand Up @@ -21306,6 +21338,7 @@ mmap.enablePathFinding = 0
wow_core::guid::HighGuid::Vehicle
);
assert_eq!(creature.guid().counter(), 1);
assert_ne!(creature.guid().counter(), spawn_id as i64);
assert_eq!(creature.guid().entry(), entry);
assert_eq!(creature.lifecycle_metadata().spawn_id, spawn_id);
assert_eq!(creature.lifecycle_metadata().vehicle_id, Some(101));
Expand Down Expand Up @@ -21521,6 +21554,13 @@ mmap.enablePathFinding = 0
display_id_other_gender: 0,
is_trigger: false,
},
wow_data::CreatureModelInfoLikeCpp {
display_id: 999,
bounding_radius: 0.0,
combat_reach: 1.5,
display_id_other_gender: 0,
is_trigger: false,
},
])),
creature_equipment_store: Arc::new(wow_data::CreatureEquipmentStoreLikeCpp::default()),
creature_addon_store: Arc::new(wow_data::CreatureAddonStoreLikeCpp::default()),
Expand Down Expand Up @@ -21627,6 +21667,34 @@ mmap.enablePathFinding = 0
super::spawn_store_loader::CanonicalSpawnMetadataLikeCpp::new(store, templates)
}

fn test_spawn_metadata_with_explicit_spawn_ids<const N: usize>(
groups: [(u32, u32, SpawnGroupFlags, u64); N],
) -> super::spawn_store_loader::CanonicalSpawnMetadataLikeCpp {
let mut store = SpawnStore::new();
let mut templates = BTreeMap::new();
let mut rows = Vec::new();
for (group_id, map_id, flags, spawn_id) in groups {
templates.insert(
group_id,
SpawnGroupTemplateData {
group_id,
name: format!("test group {group_id}"),
map_id: wow_map::spawn::SPAWNGROUP_MAP_UNSET,
flags,
},
);
let spawn = test_spawn(spawn_id, map_id);
store.add_object_spawn(&spawn, |_| false);
rows.push(SpawnGroupMemberRow {
group_id,
spawn_type: SpawnObjectType::Creature as u8,
spawn_id,
});
}
store.apply_spawn_groups_like_cpp(&mut templates, rows);
super::spawn_store_loader::CanonicalSpawnMetadataLikeCpp::new(store, templates)
}

fn test_spawn(spawn_id: u64, map_id: u32) -> SpawnData {
SpawnData {
object_type: SpawnObjectType::Creature,
Expand Down
Loading
Loading