diff --git a/docs/quickstart.md b/docs/quickstart.md index 8318846b8..b0b33df77 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -400,8 +400,9 @@ It prints the notice above in full and waits for you to type `yes`. Then it pull (checksum-verified against the pinned commit), confirms the client path, and runs the world ETL plus the curated class-spell trainer offerings on every content destination. The sharded plan is `lyracore` with `alliance-eastern`, `lyracore-kalimdor` with `alliance-kalimdor`, and -`lyracore-instances` with `instances`. Under `--single`, `lyracore` receives `alliance-single`, the -union of both continents and map 36. Pass `--accept` to answer the consent question in advance for a +`lyracore-instances` with `instances`. The Eastern profiles include all of Deeprun Tram, map 369, +on the default World Shard. Under `--single`, `lyracore` receives `alliance-single`, the +union of both continents, map 36 and map 369. Pass `--accept` to answer the consent question in advance for a scripted run, and leave `--client-data` off to be prompted for the path. **What to expect.** The pull is a few hundred megabytes of git history. Each ETL is the long diff --git a/docs/research/deeprun-tram-objects.md b/docs/research/deeprun-tram-objects.md new file mode 100644 index 000000000..a1c03586b --- /dev/null +++ b/docs/research/deeprun-tram-objects.md @@ -0,0 +1,96 @@ +# Deeprun Tram placed objects + +Research for [issue 648](https://github.com/LyraCoreProject/LyraCore/issues/648), checked against +LyraCore commit `46bf948b27b6d21555832b7505ce06ba5fae4f48` on 2026-10-10. This note records source +inspection. It does not establish the deployed import configuration or a real client's result. + +## Finding and fix scope + +The canonical World Import Profiles omit map 369. Add whole-map coverage to `alliance-eastern`, +`starting-eastern`, and `alliance-single`. These profiles own the Eastern Kingdoms content that +connects to Deeprun Tram. Keep the import script's planned map lists consistent. Existing code can +import the placed objects and deliver their static CREATE packets. No new AOI mechanism is justified +by the evidence. Moving tram cars and client scenery need separate checks. + +The profile catalogue currently covers bounded parts of maps 0 and 1. Only `alliance-single` and +`instances` include a whole map, map 36. The spawn predicate accepts every coordinate on a whole map, +so map 369 should use that path rather than a continent rectangle. The same predicate controls +creature and gameobject coverage. +[Profile catalogue](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/importer/src/world_import_scope.rs#L160-L198), +[scope predicate](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/importer/src/world_import_scope.rs#L288-L302), +[gameobject filter](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/importer/src/main.rs#L4918-L4956). + +## What the external source proves + +The issue's pinned CMaNGOS ClassicDB update adds a Deeprun Chest and replaces 88 Subway Bench spawns +on map 369. It also corrects other placed objects. This proves that some station objects require +gameobject rows. An update file is neither a complete census nor evidence of what an Operator's +assembled dump contains. +[ClassicDB update](https://github.com/cmangos/classic-db/blob/ec4f596146be6467ea93c57397858e329e2db852/Updates/4790_WDB-ironforge_deepruntram_fixes.sql#L35-L141). + +Transport behavior needs more than static placement. CMaNGOS creates a separate elevator transport +for its transport type and sets transport flags, an update flag, pause time, and initial state. +LyraCore's existing CREATE builder always emits a stationary position, zero gameobject flags, and +`has_transport: 0`. Those differences justify a separate tram rendering and movement check. They do +not establish which missing object the play report meant. +[CMaNGOS creation](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Entities/GameObject.cpp#L101-L106), +[transport fields](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Entities/GameObject.cpp#L194-L247), +[LyraCore CREATE](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/codec/gameobject.rs#L58-L93). + +Client terrain and model geometry have a different input path, the Operator's client archives. +The object update does not prove missing archive scenery. Whole-map spawn coverage also adds no +terrain, navigation, or vmap extraction by itself. Those modes iterate explicit bounded slices or +Instance Vmap Slices. Keep geometry changes outside this fix unless archive or client evidence +identifies another defect. +[Ingestion sources](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/docs/data-ingestion.md#L34-L54), +[terrain iteration](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/importer/src/terrain.rs#L181-L203), +[navigation iteration](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/importer/src/nav.rs#L710-L734), +[vmap iteration](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/importer/src/vmap.rs#L458-L478). + +## Shard assignment and CREATE delivery + +The documented Shard Map sends map 1 to the Kalimdor World Shard and map 36 to the Instance Pool. +Unmatched maps resolve to the default World Shard, so map 369 belongs with the eastern import under +that topology. The shared dungeon catalogue contains only map 36. This is a conclusion about the +documented topology, not the live Realm's settings. A custom rule for map 369 must have matching +content on its owning Shard. +[Documented rules](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/docs/architecture.md#L330-L340), +[Shard Map resolution](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/config.rs#L203-L211), +[dungeon catalogue](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/crates/lyracore-shared/src/instance.rs#L1-L15). + +The script independently lists planned maps and uses them for spatial ownership checks. Adding 369 +only to the Rust catalogue leaves that contract inconsistent. +[Profile map lists](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/importer/scripts/import-manifest.sh#L38-L45), +[expected maps](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/importer/scripts/import-world.sh#L259-L267). + +The importer retains the spawn's map, position, and quaternion and emits templates for used entries. +The Module loads static gameobjects into instance 0 and computes their AOI cells. The Gateway +subscribes to gameobjects and their templates. Insert, recenter, and resident sweep paths reach the +same Relay. That Relay requires a matching instance and a template on the holding Shard, then emits +CREATE followed by rotation VALUES. A spawn without its template produces no CREATE. +[Import rows and templates](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/importer/src/main.rs#L4930-L5035), +[Module loading](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/module/src/gameobject.rs#L1131-L1175), +[subscriptions](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/stdb/connection.rs#L1298-L1299), +[insert dispatch](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/stdb/world_view.rs#L1703-L1731), +[recenter and sweep](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/stdb/world_view.rs#L6141-L6208), +[CREATE Relay](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/stdb/subscriptions.rs#L1130-L1177). + +## Verification and remaining limits + +Add a planner test with authored map-369 objects at both station distances. Assert that eastern and +single profiles retain both spawns and templates, preserve map and rotation, and exclude them from +Kalimdor and Instance Pool profiles. Update profile catalogue and shell manifest checks. Existing +tests already cover whole-map spawn preservation, quaternion packing, CREATE serialization, and AOI +partition behavior. No external SQL rows or emulator code need to enter the test fixtures. +[Whole-map and quaternion tests](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/importer/src/main.rs#L8040-L8133), +[CREATE tests](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/codec/gameobject.rs#L199-L340), +[AOI comparison](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/stdb/world_index.rs#L635-L687). + +No local assembled dump appeared in `.import/`. This research ran no import, tests, or live reads. +Offline tests can prove import planning and packet construction. They cannot prove deployed content, +visual rendering, or tram movement. After an authorized reimport, record the source stamp, owning +Shard, map-369 spawn count, and template completeness. A Headless Client can check CREATE delivery. +A real build-5875 client must inspect identifiable placed objects at both entrances and check tram +cars and archive scenery separately. The repository's Verification ladder requires these distinct +checks. +[Verification ladder](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/docs/architecture.md#L706-L724). diff --git a/importer/scripts/import-manifest.sh b/importer/scripts/import-manifest.sh index bdc67207a..16f0b24a9 100644 --- a/importer/scripts/import-manifest.sh +++ b/importer/scripts/import-manifest.sh @@ -39,9 +39,9 @@ # names and planned maps intentionally carry no rectangles or samples: world_import_scope.rs is the # sole catalogue for spatial scope. The map lists are only the script's post-import ownership check. WORLD_PROFILES="alliance-eastern alliance-kalimdor alliance-single instances" -PROFILE_ALLIANCE_EASTERN_MAPS="0" +PROFILE_ALLIANCE_EASTERN_MAPS="0 369" PROFILE_ALLIANCE_KALIMDOR_MAPS="1" -PROFILE_ALLIANCE_SINGLE_MAPS="0 1 36" +PROFILE_ALLIANCE_SINGLE_MAPS="0 1 36 369" PROFILE_INSTANCES_MAPS="36" # MAP-AWARENESS: every count floor below is tuned to the MAP-0 Elwynn/Westfall box. A continent import diff --git a/importer/scripts/import-world-sql-test.sh b/importer/scripts/import-world-sql-test.sh index 7f830bd91..04b8524c7 100644 --- a/importer/scripts/import-world-sql-test.sh +++ b/importer/scripts/import-world-sql-test.sh @@ -51,6 +51,7 @@ checkout="$sandbox/checkout" mkdir -p "$checkout/importer/scripts" "$checkout/target/debug" cp "$repo_root/importer/scripts/import-world.sh" "$checkout/importer/scripts/import-world.sh" cp "$repo_root/importer/scripts/import-manifest.sh" "$checkout/importer/scripts/import-manifest.sh" +cp "$repo_root/importer/scripts/classic-db.lock" "$checkout/importer/scripts/classic-db.lock" cp "$repo_root/importer/scripts/import-class-spells.sh" "$checkout/importer/scripts/import-class-spells.sh" chmod +x "$checkout/importer/scripts/import-world.sh" "$checkout/importer/scripts/import-class-spells.sh" under_test="$checkout/importer/scripts/import-world.sh" @@ -194,7 +195,7 @@ echo "import-world.sh — canonical profile argument" STUB_MODE=succeed STUB_ROW_COUNT=150 WORLD_PROFILE=alliance-single run_import check "the profile run reaches the assertion stage" not_contains "$out" '[world] ABORT' check "the profile is displayed with all of its planned maps" \ - contains "$out" 'canonical profile alliance-single (planned maps: 0 1 36)' + contains "$out" 'canonical profile alliance-single (planned maps: 0 1 36 369)' check "the dump ETL receives the canonical profile name" \ grep -q -- 'lyracore-importer .*--world-profile alliance-single' "$stub_log" check "the dump ETL does not recreate a profile as legacy spatial flags" \ diff --git a/importer/src/main.rs b/importer/src/main.rs index 5d41532ba..8242b551f 100644 --- a/importer/src/main.rs +++ b/importer/src/main.rs @@ -7756,6 +7756,58 @@ mod tests { ); } + #[test] + fn canonical_profiles_import_deeprun_tram_objects_only_on_the_serving_shard() { + let dump = "INSERT INTO `gameobject_template` VALUES \ + (500,5,100,'Station Prop',0,0,0,1,0,0,0,0),\ + (501,5,101,'Tunnel Prop',0,0,0,1,0,0,0,0),\ + (502,5,102,'Other Map Prop',0,0,0,1,0,0,0,0); \ + INSERT INTO `gameobject` VALUES \ + (10,500,369,0,10,20,-5,0,0,0,0,1),\ + (11,501,369,0,10,2200,-5,0,0,0,0,1),\ + (12,502,370,0,10,20,-5,0,0,0,0,1);"; + + for (profile, serves_tram) in [ + ("alliance-eastern", true), + ("starting-eastern", true), + ("alliance-single", true), + ("alliance-kalimdor", false), + ("starting-kalimdor", false), + ("instances", false), + ] { + let args = parse_args_from([ + "--dump", + "synthetic.sql", + "--family", + "gameobjects", + "--world-profile", + profile, + ]) + .expect("profile command"); + let plan = build_dump_plan(dump, &args, &None, &None).expect("profile dump plan"); + let expected_count = if serves_tram { 2 } else { 0 }; + assert_eq!(plan.go_row_count, expected_count, "{profile} spawn count"); + let spawns = plan.go_batches.join(";"); + let templates = plan.stmts.join(";"); + for (entry, display, name, y) in [ + (500, 100, "Station Prop", 20), + (501, 101, "Tunnel Prop", 2200), + ] { + assert_eq!( + spawns.contains(&format!(",{entry},369,10,{y},-5,0,0,0,0,0,1")), + serves_tram, + "{profile} spawn {entry} keeps its map, position and rotation" + ); + assert_eq!( + templates.contains(&format!("({entry},5,{display},'{name}',")), + serves_tram, + "{profile} includes the template for spawn {entry}" + ); + } + assert!(!templates.contains("'Other Map Prop'"), "{profile}"); + } + } + #[test] fn canonical_eastern_command_keeps_two_disjoint_map_zero_corridors_and_drops_the_gap() { let dump = format!( diff --git a/importer/src/world_import_scope.rs b/importer/src/world_import_scope.rs index 344e2df4c..37626554c 100644 --- a/importer/src/world_import_scope.rs +++ b/importer/src/world_import_scope.rs @@ -160,7 +160,7 @@ impl WorldImportScope { let (bounded_slices, whole_maps, instance_vmap_slices, forced_creature_entries) = match profile { WorldImportProfile::AllianceEastern => { - (eastern()?, vec![], vec![], eastern_forced_creatures()) + (eastern()?, vec![369], vec![], eastern_forced_creatures()) } WorldImportProfile::AllianceKalimdor => (kalimdor()?, vec![], vec![], vec![]), WorldImportProfile::StartingEastern => { @@ -171,7 +171,7 @@ impl WorldImportScope { (1_300.0, 2_300.0, 900.0, 2_200.0), (1_814.55, 1_485.20, 90.66), )?); - (slices, vec![], vec![], eastern_forced_creatures()) + (slices, vec![369], vec![], eastern_forced_creatures()) } WorldImportProfile::StartingKalimdor => { let mut slices = kalimdor()?; @@ -192,7 +192,12 @@ impl WorldImportScope { WorldImportProfile::AllianceSingle => { let mut slices = eastern()?; slices.extend(kalimdor()?); - (slices, vec![36], deadmines(), eastern_forced_creatures()) + ( + slices, + vec![36, 369], + deadmines(), + eastern_forced_creatures(), + ) } WorldImportProfile::Instances => (vec![], vec![36], deadmines(), vec![]), }; @@ -511,7 +516,7 @@ mod tests { .expect("eastern profile"); assert_eq!(eastern.name(), "alliance-eastern"); assert_eq!(eastern.bounded_slices.len(), 3); - assert!(eastern.whole_maps.is_empty()); + assert_eq!(eastern.whole_maps, vec![369]); assert!(eastern.instance_vmap_slices.is_empty()); assert_eq!( eastern.forced_creature_entries, @@ -530,7 +535,7 @@ mod tests { let single = WorldImportScope::canonical(WorldImportProfile::AllianceSingle) .expect("single profile"); assert_eq!(single.bounded_slices.len(), 5); - assert_eq!(single.whole_maps, vec![36]); + assert_eq!(single.whole_maps, vec![36, 369]); assert_eq!(single.instance_vmap_slices.len(), 1); assert_eq!( single.forced_creature_entries,