The realm's flight network is bigger than 1.12's, and a second capture point
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -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]);
|
||||
|
||||
@@ -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<String> = 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();
|
||||
|
||||
@@ -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<u32> = 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<u32> = 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<u32, u32> = 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<u32> = 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<u32>>(),
|
||||
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<u32>>(),
|
||||
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<u32> = 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");
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -83,16 +83,19 @@ pub fn load_taxi_paths(chain: &mut Chain) -> Result<TaxiPaths> {
|
||||
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");
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user