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:
2026-10-31 19:20:54 -06:00
co-authored by Claude Opus 5
parent e58ddc10ab
commit e61976bc0e
5 changed files with 153 additions and 33 deletions
+27 -22
View File
@@ -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(),
+72 -7
View File
@@ -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!(
+36 -2
View File
@@ -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"
);
}
}