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
5 changes: 3 additions & 2 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
96 changes: 96 additions & 0 deletions docs/research/deeprun-tram-objects.md
Original file line number Diff line number Diff line change
@@ -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).
4 changes: 2 additions & 2 deletions importer/scripts/import-manifest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion importer/scripts/import-world-sql-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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" \
Expand Down
52 changes: 52 additions & 0 deletions importer/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down
15 changes: 10 additions & 5 deletions importer/src/world_import_scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand All @@ -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()?;
Expand All @@ -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![]),
};
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Loading