From 266af6eb26a0a5f0016d8ae38072c4f2ec6e97ae Mon Sep 17 00:00:00 2001 From: grom Date: Sat, 31 Oct 2026 17:31:44 -0600 Subject: [PATCH] The realm's flight network is bigger than 1.12's, and a second capture point MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All of today's update, measured rather than assumed. `TaxiNodes.dbc` 85 -> 122 rows (37 new nodes, the boat and canoe routes among them) and `TaxiPath` 287 -> 364 hops, field counts unchanged in both — which is the half that would have broken the decode. What moved with them: - **Stormwind's node is "Stormwind, Elwynn Forest"**, where stock clipped it to "Elwynn". - **The Stormwind -> Sentinel Hill hop is id 1**, not 6; id 6 is now Stormwind -> Ironforge at 50 copper. The 110-copper fare did not move. - **Path 302 is 23 nodes and stays on Eastern Kingdoms**, where 1.12's was 36 and crossed. The cross-continent property and the flag-2 landing stops both moved onto the paths that cross, so they are pinned on path 72 now — 37 nodes over maps {0,1}, two stops at node indexes 11 and 29, 60 seconds each. That a path *can* cross is the thing the decode has to keep answering, since the map is per node and not per path. 302 and 285 are asserted as the straight runs they now are. - **One node is genuinely outside its continent's taxi rectangle**: `Canoe: Icepoint Rock - Darkshore` sits at x = 13651.5 where Kalimdor's `WorldMapContinent.dbc` box ends at 12470, so it projects 4.85% below the map. `project` is the reference's own arithmetic, so the real client draws that blip off the art too — it joins Programmer Isle as a named exception rather than a loosened tolerance. The other 121 rows are inside. And Eastern Plaguelands' `WorldStateUI` readout is four rows, not three: the realm added a second `CAPTUREPOINT`, scoped to the whole of map 0. Its StateVariable is unset in the fixture, so its `state` is 0 — and handing it up anyway is right, because the filtering is the reference's own Lua (`WorldStateFrame_Update`'s `if ( state > 0 )`, `WorldStateFrame.lua:41`). The zone-scoping test now asserts what actually matters: no *live* row escapes its zone. All seven gates green. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn --- crates/benilla-app/src/ui_taxi/routing.rs | 16 +++++- crates/benilla-app/src/world_state_ui.rs | 24 +++++++- crates/benilla-formats/src/taxi.rs | 69 ++++++++++++++++------- crates/benilla-formats/src/taxi_nodes.rs | 6 +- crates/benilla-formats/src/taxi_path.rs | 9 ++- crates/benilla-formats/tests/chain.rs | 10 +++- 6 files changed, 102 insertions(+), 32 deletions(-) diff --git a/crates/benilla-app/src/ui_taxi/routing.rs b/crates/benilla-app/src/ui_taxi/routing.rs index 6dd97a1c..80e8e381 100644 --- a/crates/benilla-app/src/ui_taxi/routing.rs +++ b/crates/benilla-app/src/ui_taxi/routing.rs @@ -429,6 +429,14 @@ mod tests { if n.id == 3 { continue; // Programmer Isle } + // The realm's own canoe node, and genuinely outside the continent's taxi + // rectangle rather than a projection fault: Kalimdor's `WorldMapContinent.dbc` + // box ends at x = 12470 and this node sits at x = 13651.5, 1181 yards past it. + // `project` is the reference's own arithmetic, so the real client draws this blip + // a few per cent below the map art too. Every other one of the 122 rows is inside. + if n.id == 119 { + continue; // Canoe: Icepoint Rock - Darkshore + } let (x, y) = project(cont, n.pos[0], n.pos[1]); assert!( (0.02..=0.98).contains(&x) && (0.02..=0.98).contains(&y), @@ -444,7 +452,9 @@ mod tests { let ironforge = nodes.get(6).expect("Ironforge"); let menethil = nodes.get(7).expect("Menethil Harbor"); let lakeshire = nodes.get(5).expect("Lakeshire"); - assert_eq!(stormwind.name, "Stormwind, Elwynn"); + // "Elwynn Forest", not stock's clipped "Elwynn": the realm spells the zone out. The other + // three rows read as 1.12 wrote them. + assert_eq!(stormwind.name, "Stormwind, Elwynn Forest"); assert_eq!(ironforge.name, "Ironforge, Dun Morogh"); assert_eq!(menethil.name, "Menethil Harbor, Wetlands"); assert_eq!(lakeshire.name, "Lakeshire, Redridge"); @@ -464,7 +474,7 @@ mod tests { ); } - /// The real Stormwind (2) to Sentinel Hill (4) hop, `TaxiPath` id 6 at 110 copper; with every + /// The real Stormwind (2) to Sentinel Hill (4) hop, `TaxiPath` id 1 at 110 copper (stock numbered it 6); with every /// node known, the ones Stormwind cannot route to are left out. #[test] fn build_nodes_classifies_a_real_known_hop() { @@ -486,7 +496,7 @@ mod tests { let sw_idx = ui .iter() - .position(|n| n.name == "Stormwind, Elwynn") + .position(|n| n.name == "Stormwind, Elwynn Forest") .expect("Stormwind visible"); assert_eq!(ui[sw_idx].node_type, TaxiNodeType::Current); assert_eq!(resolved[sw_idx].chain, vec![2]); diff --git a/crates/benilla-app/src/world_state_ui.rs b/crates/benilla-app/src/world_state_ui.rs index 957542be..1d85cee6 100644 --- a/crates/benilla-app/src/world_state_ui.rs +++ b/crates/benilla-app/src/world_state_ui.rs @@ -331,8 +331,8 @@ mod tests { let rows = build(&catalog, &states, true); assert_eq!( rows.len(), - 3, - "the two tower counters plus the progress bar" + 4, + "the two tower counters, the progress bar, and the realm's second capture point" ); assert_eq!(rows[0].text, "Towers Controlled: 3"); assert_eq!(rows[0].icon, "Interface\\WorldStateFrame\\AllianceTower"); @@ -343,6 +343,14 @@ mod tests { assert_eq!(rows[2].text, "Progress: 60"); assert_eq!(rows[2].extended_ui, "CAPTUREPOINT"); assert_eq!(rows[2].extended_ui_state, [60, 40, 0]); + + // The realm added a second `CAPTUREPOINT` row to this zone. This fixture writes no value + // for its StateVariable, so it resolves to 0 and its `ui_state` is 0 — and handing it up + // anyway is correct: `WorldStateFrame_Update`'s loop is gated on `if ( state > 0 )` + // (`WorldStateFrame.lua:41`), so the filtering is the reference's own Lua, not ours. + assert_eq!(rows[3].extended_ui, "CAPTUREPOINT"); + assert_eq!(rows[3].ui_state, 0, "unset, so the Lua skips it"); + assert_eq!(rows[3].extended_ui_state, [0, 0, 0]); assert_eq!(rows[2].ui_state, 1, "state 2426 reads 1"); // The server flips a tower. @@ -351,9 +359,19 @@ mod tests { assert_eq!(rows[0].text, "Towers Controlled: 2"); assert_eq!(rows[1].text, "Towers Controlled: 2"); + // Elsewhere on the same continent, nothing the reference would draw. Not nothing at all: + // the realm's new `CAPTUREPOINT` row is scoped to the whole of map 0, so it comes back + // here too — with its StateVariable unset, so `ui_state` is 0 and + // `WorldStateFrame_Update`'s `if ( state > 0 )` skips it (`WorldStateFrame.lua:41`). The + // property that matters is that no *live* row escapes its zone. let mut elsewhere = WorldStates::default(); elsewhere.init_scope(0, 12); // Elwynn Forest - assert!(build(&catalog, &elsewhere, true).is_empty()); + let stray: Vec = build(&catalog, &elsewhere, true) + .into_iter() + .filter(|r| r.ui_state > 0) + .map(|r| r.text) + .collect(); + assert!(stray.is_empty(), "a live row escaped its zone: {stray:?}"); // Warsong Gulch: `Type` 0, with dynamic flag icons. let mut wsg = WorldStates::default(); diff --git a/crates/benilla-formats/src/taxi.rs b/crates/benilla-formats/src/taxi.rs index 3bf8dc3e..dbf6bcb7 100644 --- a/crates/benilla-formats/src/taxi.rs +++ b/crates/benilla-formats/src/taxi.rs @@ -121,8 +121,11 @@ mod tests { cat.len() ); + // 23 nodes, where 1.12's path 302 had 36: the realm reshaped it, and it now stays on + // Eastern Kingdoms rather than crossing. `TaxiPath` grew 287 -> 364 hops and `TaxiNodes` + // 85 -> 122 in the same update, so a re-cut path is the expected kind of difference. let path302 = cat.path(302).expect("path 302 exists"); - assert_eq!(path302.len(), 36, "path 302 has 36 nodes"); + assert_eq!(path302.len(), 23, "the realm's path 302 has 23 nodes"); for w in path302.windows(2) { assert!( w[0].node_index < w[1].node_index, @@ -131,33 +134,61 @@ mod tests { w[1].node_index ); } - let maps: std::collections::BTreeSet = path302.iter().map(|n| n.map_id).collect(); + // The cross-continent property moved with it, so it is pinned on a path that still has + // it: 72 spans both maps over 37 nodes. That a path *can* cross is what the decode has to + // keep answering, since the map a node belongs to is per node, not per path. + let crossing: std::collections::BTreeSet = cat + .path(72) + .expect("path 72 exists") + .iter() + .map(|n| n.map_id) + .collect(); assert_eq!( - maps, + crossing, std::collections::BTreeSet::from([0, 1]), - "path 302 spans maps {{0,1}}" + "path 72 spans maps {{0,1}}" ); + // The per-node `flags`/`delay` pair, pinned on path 72 as well, because the realm's + // re-cut left 302 and 285 with no stop nodes at all (every node flags 0) — the landing + // stops moved onto the paths that cross, which is where they belong. 72 carries two, at + // node indexes 11 and 29, each a 60-second delay. + let crossing_path = cat.path(72).expect("path 72 exists"); let mut flag_histogram: HashMap = HashMap::new(); - for n in path302 { + for n in crossing_path { *flag_histogram.entry(n.flags).or_insert(0) += 1; } - assert_eq!(flag_histogram.get(&0).copied(), Some(34)); + assert_eq!(flag_histogram.get(&0).copied(), Some(35)); assert_eq!(flag_histogram.get(&2).copied(), Some(2)); - let flag2_delays: Vec = path302 - .iter() - .filter(|n| n.flags == 2) - .map(|n| n.delay) - .collect(); - assert_eq!(flag2_delays, vec![60, 60], "both flag-2 nodes delay 60s"); + assert_eq!( + crossing_path + .iter() + .filter(|n| n.flags == 2) + .map(|n| n.delay) + .collect::>(), + vec![60, 60], + "both flag-2 nodes delay 60s" + ); + assert_eq!( + crossing_path + .iter() + .filter(|n| n.flags == 2) + .map(|n| n.node_index) + .collect::>(), + vec![11, 29], + "path 72 stops at indexes 11, 29" + ); + // 302 and 285 keep their own sanity: a straight run with no stops. + assert!( + path302.iter().all(|n| n.flags == 0), + "the realm's path 302 is a straight run" + ); let path285 = cat.path(285).expect("path 285 exists"); - assert_eq!(path285.len(), 27, "path 285 has 27 nodes"); - let stop_indexes: Vec = path285 - .iter() - .filter(|n| n.flags == 2) - .map(|n| n.node_index) - .collect(); - assert_eq!(stop_indexes, vec![4, 20], "path 285 stops at indexes 4, 20"); + assert_eq!(path285.len(), 21, "the realm's path 285 has 21 nodes"); + assert!( + path285.iter().all(|n| n.flags == 0), + "and so is 285 — stock had it stopping at indexes 4 and 20" + ); let path292 = cat.path(292).expect("path 292 exists"); assert!(!path292.is_empty(), "path 292 is non-empty"); diff --git a/crates/benilla-formats/src/taxi_nodes.rs b/crates/benilla-formats/src/taxi_nodes.rs index 8e95a07b..7b8c52f2 100644 --- a/crates/benilla-formats/src/taxi_nodes.rs +++ b/crates/benilla-formats/src/taxi_nodes.rs @@ -108,10 +108,12 @@ mod tests { let data = crate::wow_data_or_skip!(); let mut chain = crate::open_chain(&data).expect("open chain"); let cat = load_taxi_nodes(&mut chain).expect("load TaxiNodes"); - assert_eq!(cat.len(), 85, "1.12 ships 85 taxi nodes"); + // 122, not 1.12's 85: the realm adds 37 nodes, its boat and canoe routes among them. + assert_eq!(cat.len(), 122, "the realm ships 122 taxi nodes"); let stormwind = cat.get(2).expect("node 2 exists"); - assert_eq!(stormwind.name, "Stormwind, Elwynn"); + // The realm spells the zone out where stock clipped it to "Elwynn". + assert_eq!(stormwind.name, "Stormwind, Elwynn Forest"); assert_eq!(stormwind.map_id, 0, "Eastern Kingdoms"); assert_eq!(stormwind.mount_horde, 0, "no Horde service from Stormwind"); assert_eq!( diff --git a/crates/benilla-formats/src/taxi_path.rs b/crates/benilla-formats/src/taxi_path.rs index 95635a3b..f3f63346 100644 --- a/crates/benilla-formats/src/taxi_path.rs +++ b/crates/benilla-formats/src/taxi_path.rs @@ -83,16 +83,19 @@ pub fn load_taxi_paths(chain: &mut Chain) -> Result { mod tests { use super::*; - /// Hop 6 runs Stormwind (node 2) to Sentinel Hill (node 4) for 110 copper. + /// Hop **1** runs Stormwind (node 2) to Sentinel Hill (node 4) for 110 copper. Stock 1.12 had + /// that hop as id 6; the realm renumbered when it added its 77 hops, and 6 is now Stormwind to + /// Ironforge at 50 copper. The fare is the part that did not move. #[test] fn real_taxi_path_layout_sanity() { let data = crate::wow_data_or_skip!(); let mut chain = crate::open_chain(&data).expect("open chain"); let cat = load_taxi_paths(&mut chain).expect("load TaxiPath"); - assert_eq!(cat.len(), 287, "1.12 ships 287 taxi path hops"); + // 364, not 1.12's 287: the realm adds 77 hops alongside its 37 new nodes. + assert_eq!(cat.len(), 364, "the realm ships 364 taxi path hops"); let hop = cat.between(2, 4).expect("a direct 2 -> 4 hop exists"); - assert_eq!(hop.id, 6); + assert_eq!(hop.id, 1); assert_eq!(hop.cost, 110); assert!(hop.cost > 0, "a real hop always carries a nonzero fare"); diff --git a/crates/benilla-formats/tests/chain.rs b/crates/benilla-formats/tests/chain.rs index 009eb058..381df58e 100644 --- a/crates/benilla-formats/tests/chain.rs +++ b/crates/benilla-formats/tests/chain.rs @@ -199,7 +199,13 @@ fn dumps_taxinodes_to_csv() { let (records, fields) = benilla_formats::dbc_to_csv(&bytes, "TaxiNodes.dbc", &out).expect("dbc->csv"); - assert_eq!((records, fields), (85, 16), "vanilla TaxiNodes.dbc shape"); + // 122 rows, not vanilla's 85: the realm adds 37 flight nodes — its boat and canoe routes + // among them. The 16 fields are unchanged, which is the half that would break the decode. + assert_eq!( + (records, fields), + (122, 16), + "the realm's TaxiNodes.dbc shape" + ); let csv = std::fs::read_to_string(&out).expect("read csv"); assert!( csv.lines() @@ -209,7 +215,7 @@ fn dumps_taxinodes_to_csv() { "schema-derived header" ); assert!(csv.contains("Stormwind"), "expected a known flight node"); - assert_eq!(csv.lines().count(), 86, "85 records + 1 header row"); + assert_eq!(csv.lines().count(), 123, "122 records + 1 header row"); let _ = std::fs::remove_file(&out); }