formats: the world-map goldens, and a nondeterministic continent lookup
The four map tables against the realm's own files. One real bug, and three findings that are the realm's data rather than ours. **`WorldMapArea::continent` was a coin flip.** It `find`s the `area_id == 0` row for a map id, and 5875 had exactly one per map so order never mattered. The realm ships two for map 0 — 14 "Azeroth" and its own 694 "World" — and the lookup iterated the `by_id` `HashMap`, whose order is seeded per process. So `continent(0)` answered Azeroth or World depending on the run. Now it walks file order, which is the reference's own walk and picks 1.12's row; the test asserts 14 and passes eight runs in a row where it used to flip. - **WorldMapArea** 51 -> 132 rows loaded, of 175 in the file. The 43 dropped are dropped on purpose: `AreaName` empty, `AreaID` 0, the whole rect zero, one per instance map (33 Shadowfang through 533 Naxxramas, then the realm's 800s). An instance has no continent art — but `AreaID == 0` is exactly what `ui_world_map.rs:146` reads as "this row is a continent", so keeping them would put 43 unnamed continents in `GetMapContinents()`. `str_at` folds an empty string to `None` and the row falls out; the test now pins that as deliberate. The realm's third *named* continent is 694 "World", a second sheet over map 0 whose art ships (patch-M) but whose four rect columns are all zero — the only named row like that. `map_proj::zone_uv` already refuses a zero span and answers 0.0 instead of dividing, so that sheet puts a blip in the corner rather than NaN. - **WorldMapContinent** 2 -> 3 rows. The new row's `MapID` is 42, which the same patch *deleted* from `Map.dbc` along with the rest of 1.12's dev test maps (13 "Testing", 25 "Scott Test", 29 "CashTest", 37 "Azshara Crater", 42 "Collin's Test", 44 "<unused> Monastery"). It is inert rather than broken: the continent list comes from `WorldMapArea`, and this table is only ever reached by `wmc.get(map_id)` for maps 0 and 1, so nothing asks for continent 42. - **The zone bitmaps are repainted.** `Azeroth.zmp` and `Kalimdor.zmp` have the realm's own copies in patch-3 and patch-4, and every id they write is a **zone** where 1.12 wrote sub-areas and left the client to roll them up. Azeroth: 1874 labelled cells over 34 distinct ids, all zone-level, against 2630 over 334 of which 310 were sub-areas (Northshire Valley, Crystal Lake, Fargodeep Mine). After the one-hop rollup `ui_world_map.rs:193` applies, 1280 cells agree, 534 name a *different* zone, 816 lost a zone and 60 gained one — a redrawn map, not a mechanical flattening. Nothing of ours needed changing: the rollup is a no-op on an already-flat grid. The test asserts the structural claim behind the number (every labelled cell is a top-level zone), which 1.12's own file would fail, so a decode slip cannot hide inside a re-recorded count. - **WorldMapOverlay** 526 -> 707, and not an extension: 360 ids added, 179 removed, 347 survivors given a different texture and rect. None of this table's numbering is 1.12's, so only the joins are worth asserting. `dbc_diff` gained `--row=N`, which dumps a row from the winning copy *and* the baseline — the only way to read a row the realm deleted, which is how the six test maps above got their names. benilla-formats: 25 -> 21 failures, no regressions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -134,28 +134,6 @@ fn main() -> Result<()> {
|
||||
// `copies` is ascending priority, so the client reads the last one.
|
||||
let win = open(copies[copies.len() - 1].0)?;
|
||||
|
||||
// `--row=N` just prints that row of the winning copy, text slots as text: for naming the thing
|
||||
// behind an id a decoded catalog only gave a number for.
|
||||
if let Some(want) = row_arg {
|
||||
match win.rows.get(&want) {
|
||||
Some(r) => {
|
||||
let cells: Vec<String> = r
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(f, &v)| {
|
||||
if win.is_text(v) {
|
||||
format!("[{f}] {:?}", win.text(v))
|
||||
} else {
|
||||
format!("[{f}] {v}")
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
println!("\nrow {want}: {}", cells.join(" "));
|
||||
}
|
||||
None => println!("\nno row {want}"),
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
let base = match base_arg {
|
||||
Some(ref want) => copies
|
||||
.iter()
|
||||
@@ -174,6 +152,33 @@ fn main() -> Result<()> {
|
||||
file_name(base.0)
|
||||
);
|
||||
let old = open(base.0)?;
|
||||
|
||||
// `--row=N` prints that row of the winning copy and of the baseline, text slots as text: for
|
||||
// naming the thing behind an id a decoded catalog only gave a number for, and for reading a row
|
||||
// the realm *deleted*, which is only legible in the copy underneath.
|
||||
if let Some(want) = row_arg {
|
||||
let dump = |t: &Table, label: &str| match t.rows.get(&want) {
|
||||
Some(r) => {
|
||||
let cells: Vec<String> = r
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(f, &v)| {
|
||||
if t.is_text(v) {
|
||||
format!("[{f}] {:?}", t.text(v))
|
||||
} else {
|
||||
format!("[{f}] {v}")
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
println!("{label} row {want}: {}", cells.join(" "));
|
||||
}
|
||||
None => println!("{label} has no row {want}"),
|
||||
};
|
||||
println!();
|
||||
dump(&win, "wins ");
|
||||
dump(&old, "under");
|
||||
return Ok(());
|
||||
}
|
||||
println!(
|
||||
"\nwins: {} rows x {} fields under: {} rows x {} fields",
|
||||
win.rows.len(),
|
||||
|
||||
@@ -44,12 +44,16 @@ impl WorldMapAreaCatalog {
|
||||
self.by_id.get(&id)
|
||||
}
|
||||
|
||||
/// The continent-wide row (`area_id == 0`) for `map_id`, one per continent in 5875.
|
||||
/// The continent-wide row (`area_id == 0`) for `map_id`: the **first** in file order, as the
|
||||
/// reference's own walk takes it.
|
||||
///
|
||||
/// 5875 had one such row per map, so the order never mattered. The realm ships two for map 0 —
|
||||
/// 14 "Azeroth" and its own 694 "World" — and this used to `find` over the `by_id` `HashMap`,
|
||||
/// which made the answer a coin flip between them from run to run. File order settles it on
|
||||
/// Azeroth, the row 1.12 shipped.
|
||||
pub fn continent(&self, map_id: u32) -> Option<(u32, &WorldMapArea)> {
|
||||
self.by_id
|
||||
.iter()
|
||||
self.iter()
|
||||
.find(|(_, a)| a.map_id == map_id && a.area_id == 0)
|
||||
.map(|(&id, a)| (id, a))
|
||||
}
|
||||
|
||||
/// Every row as `(id, row)`, in file order.
|
||||
@@ -131,13 +135,70 @@ pub fn load_world_map_area_catalog(chain: &mut Chain) -> Result<WorldMapAreaCata
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The 5875 table: both continents, Durotar's rect, and `AreaName` naming a real art folder.
|
||||
/// The continents, Durotar's rect, and `AreaName` naming a real art folder.
|
||||
///
|
||||
/// The realm's table is 175 rows where 5875 had 51, and 43 of them are dropped on purpose:
|
||||
/// `AreaName` empty, `AreaID` 0 and the whole rect zero, one per instance map (33 Shadowfang
|
||||
/// through 533 Naxxramas, then the realm's own 800s). An instance has no continent art, so
|
||||
/// there is nothing for the row to name — but `AreaID == 0` is exactly what
|
||||
/// `ui_world_map.rs:146` reads as "this row is a continent", so keeping them would put 43
|
||||
/// unnamed continents in `GetMapContinents()`. [`crate::dbc::str_at`] folds an empty string to
|
||||
/// `None` and the loader's `else { continue }` drops the row, which is the behaviour this test
|
||||
/// pins; it is deliberate, not incidental.
|
||||
#[test]
|
||||
fn real_world_map_area_has_continents_and_durotar_and_art_folder() {
|
||||
let data = crate::wow_data_or_skip!();
|
||||
let mut chain = crate::open_chain(&data).expect("open chain");
|
||||
let cat = load_world_map_area_catalog(&mut chain).expect("load WorldMapArea");
|
||||
assert_eq!(cat.len(), 51, "all 51 rows load");
|
||||
assert_eq!(
|
||||
cat.len(),
|
||||
132,
|
||||
"the named rows load; 5875 had all 51 of its"
|
||||
);
|
||||
|
||||
// The three continent rows, and the third is the realm's: a second sheet over map 0 called
|
||||
// "World". 5875 had only Kalimdor and Azeroth, so `GetMapContinents()` grew an entry.
|
||||
let mut continents: Vec<(u32, u32, &str)> = cat
|
||||
.iter()
|
||||
.filter(|(_, a)| a.area_id == 0)
|
||||
.map(|(id, a)| (id, a.map_id, a.name.as_str()))
|
||||
.collect();
|
||||
continents.sort_unstable();
|
||||
assert_eq!(
|
||||
continents,
|
||||
vec![(13, 1, "Kalimdor"), (14, 0, "Azeroth"), (694, 0, "World")]
|
||||
);
|
||||
|
||||
// Nothing nameless survived, and no surviving row carries a degenerate rect — the two
|
||||
// properties the drop exists to keep.
|
||||
assert!(
|
||||
cat.iter().all(|(_, a)| !a.name.is_empty()),
|
||||
"a nameless row reached the catalog"
|
||||
);
|
||||
// "World" is named and has art — `Interface\WorldMap\World\World1.blp` ships in
|
||||
// patch-M — but all four of its rect columns are zero, and it is the only named row like
|
||||
// that. `map_proj::zone_uv`'s `axis` already refuses a zero span and answers 0.0 rather
|
||||
// than dividing, so selecting that continent puts a blip in the corner; it does not NaN.
|
||||
let degenerate: Vec<(u32, &str)> = cat
|
||||
.iter()
|
||||
.filter(|(_, a)| a.loc_left == a.loc_right || a.loc_top == a.loc_bottom)
|
||||
.map(|(id, a)| (id, a.name.as_str()))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
degenerate,
|
||||
vec![(694, "World")],
|
||||
"only the realm's World sheet ships a zero rect"
|
||||
);
|
||||
let world = cat.get(694).expect("row 694");
|
||||
assert_eq!(
|
||||
(
|
||||
world.loc_left,
|
||||
world.loc_right,
|
||||
world.loc_top,
|
||||
world.loc_bottom
|
||||
),
|
||||
(0.0, 0.0, 0.0, 0.0)
|
||||
);
|
||||
|
||||
let azeroth = cat.get(14).expect("Azeroth continent row (id 14)");
|
||||
assert_eq!((azeroth.map_id, azeroth.area_id), (0, 0));
|
||||
@@ -147,8 +208,12 @@ mod tests {
|
||||
assert_eq!((kalimdor.map_id, kalimdor.area_id), (1, 0));
|
||||
assert_eq!(kalimdor.name, "Kalimdor");
|
||||
|
||||
// Two rows answer map 0 now, and file order must keep picking 1.12's.
|
||||
let (azeroth_id, _) = cat.continent(0).expect("Azeroth via continent(0)");
|
||||
assert_eq!(azeroth_id, 14);
|
||||
assert_eq!(
|
||||
azeroth_id, 14,
|
||||
"not the realm's World row, which is also map 0"
|
||||
);
|
||||
let (kalimdor_id, _) = cat.continent(1).expect("Kalimdor via continent(1)");
|
||||
assert_eq!(kalimdor_id, 13);
|
||||
|
||||
|
||||
@@ -138,7 +138,20 @@ mod tests {
|
||||
let data = crate::wow_data_or_skip!();
|
||||
let mut chain = crate::open_chain(&data).expect("open chain");
|
||||
let cat = load_world_map_continent_catalog(&mut chain).expect("load WorldMapContinent");
|
||||
assert_eq!(cat.len(), 2);
|
||||
// Three rows where 5875 had two. The realm's is row 3, and its `MapID` is 42 — which the
|
||||
// same patch *deleted* from `Map.dbc`, along with the rest of 1.12's dev test maps (13
|
||||
// "Testing", 25 "Scott Test", 29 "CashTest", 37 "Azshara Crater", 42 "Collin's Test", 44
|
||||
// "<unused> Monastery"). So the row names a map that does not exist.
|
||||
//
|
||||
// It is inert rather than broken: the continent list the world map builds comes from
|
||||
// `WorldMapArea`'s `AreaID == 0` rows (`ui_world_map.rs:146`), which name maps 0 and 1
|
||||
// only, and this table is reached by `wmc.get(map_id)` for those — never walked. Nothing
|
||||
// asks for continent 42.
|
||||
assert_eq!(cat.len(), 3, "5875 shipped two continents");
|
||||
assert!(
|
||||
cat.get(42).is_some(),
|
||||
"the realm's third row is keyed on the map it deleted"
|
||||
);
|
||||
|
||||
let azeroth = cat.get(0).expect("Azeroth (mapId 0)");
|
||||
assert_eq!(
|
||||
|
||||
@@ -127,7 +127,10 @@ mod tests {
|
||||
let data = crate::wow_data_or_skip!();
|
||||
let mut chain = crate::open_chain(&data).expect("open chain");
|
||||
let cat = load_world_map_overlay_catalog(&mut chain).expect("load WorldMapOverlay");
|
||||
assert_eq!(cat.len(), 526, "all 526 rows load");
|
||||
// 707 rows where 5875 had 526, and `dbc_diff` says the realm did not extend this table but
|
||||
// rebuilt it: 360 ids added, 179 removed and 347 of the survivors given a different texture
|
||||
// and rect. So nothing here is 1.12's numbering — only the joins are worth asserting.
|
||||
assert_eq!(cat.len(), 707, "all 707 rows load; 5875 had 526");
|
||||
|
||||
let teldrassil = cat.for_area(41);
|
||||
assert!(
|
||||
|
||||
@@ -37,6 +37,17 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Cell 12735 is Goldshire: row from wx = -9450, column from wy = 60.
|
||||
///
|
||||
/// The realm repainted both bitmaps (its own copies in patch-3 and patch-4) at a coarser
|
||||
/// granularity: every id it writes is a **zone**, where 1.12 wrote sub-areas and left the
|
||||
/// client to roll them up. Azeroth carries 1874 labelled cells over 34 distinct ids, all
|
||||
/// zone-level, where `patch.MPQ` had 2630 over 334 of which 310 were sub-areas — Northshire
|
||||
/// Valley, Crystal Lake, Fargodeep Mine. Comparing the two after the one-hop parent rollup
|
||||
/// `ui_world_map.rs:193` applies: 1280 cells agree, 534 name a *different* zone, 816 lost a
|
||||
/// zone and 60 gained one, so this is a redrawn map and not a mechanical flattening.
|
||||
///
|
||||
/// Nothing of ours needed changing — the rollup is simply a no-op on an already-flat grid —
|
||||
/// and the cost is that the ~800 cells the realm left blank name no zone on hover.
|
||||
#[test]
|
||||
fn real_azeroth_zone_map_goldshire_cell() {
|
||||
let data = crate::wow_data_or_skip!();
|
||||
@@ -45,9 +56,32 @@ mod tests {
|
||||
assert_eq!(grid.len(), 16384);
|
||||
assert_eq!(grid[12735], 12, "the Goldshire cell is Elwynn Forest");
|
||||
let nonzero = grid.iter().filter(|&&v| v != 0).count();
|
||||
assert_eq!(nonzero, 1874, "labelled cells; 1.12's file had 2630");
|
||||
|
||||
// The structural claim behind that number, and the one a decode slip would break: every
|
||||
// labelled cell names a real `AreaTable` row that is itself a zone. 1.12's file fails this.
|
||||
let areas = crate::load_area_table_catalog(&mut chain).expect("load AreaTable");
|
||||
let not_a_zone: Vec<u32> = {
|
||||
let mut v: Vec<u32> = grid
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&id| id != 0)
|
||||
.filter(|&id| areas.get(id).is_none_or(|r| r.zone_id != 0))
|
||||
.collect();
|
||||
v.sort_unstable();
|
||||
v.dedup();
|
||||
v
|
||||
};
|
||||
assert!(
|
||||
(2000..6000).contains(&nonzero),
|
||||
"land cells in a plausible band, got {nonzero}"
|
||||
not_a_zone.is_empty(),
|
||||
"every labelled cell is a top-level zone: {not_a_zone:?}"
|
||||
);
|
||||
let distinct: std::collections::BTreeSet<u32> =
|
||||
grid.iter().copied().filter(|&v| v != 0).collect();
|
||||
assert_eq!(
|
||||
distinct.len(),
|
||||
34,
|
||||
"distinct zones painted; 1.12 had 334 ids"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user