The creature tables, and three things in them worth more than a count

`CreatureDisplayInfo` is 13452 rows where 5875 had 10534 and `CreatureModelData` ships different
art under the same paths, so every census over them moved. Field counts are unchanged (12 and 16),
so the layouts are intact.

Three are findings, not bookkeeping:

- **One display row dangles.** Display 185's `modelId` is 0 — it points at no `CreatureModelData`
  row at all, which 1.12 had none of. The blood-tier walk skips it because there is no model to
  read a column from, and our client falls back the same way: no model, nothing to draw. Pinned by
  id so a *second* dangling row gets looked at rather than absorbed.
- **Five races' collision boxes no longer match their columns.** The realm ships its own character
  art under unchanged paths, and the boxes moved while the DBC columns stayed: OrcMale and
  TrollMale carry a 2.031 box (HumanMale's — a reused rig) under columns of 2.361 and 2.083,
  DwarfMale 1.607 under 1.667, TrollFemale 2.427 under 1.839, GnomeFemale 1.056 under exactly 1.0.
  The column stays authoritative, which is the part that matters: the reference reads it and not
  the model (`0x60b270` → `0x6174b0`), so the depth lines follow the column and an orc's swim line
  sits a little above where its art suggests. The exception list asserts they still *dis*agree, so
  a race coming back into line gets the entry deleted instead of a stale carve-out.
- **The prism floor is exercised now, not inert.** Five `CreatureModelData` rows scale below 1
  (2602, 2719-2722 at 0.4 down to 0.2) where 1.12 had none, which is exactly where
  `max(SCALE_X, display scale)` bites. The arithmetic it exercises already has its own test in
  `entities::collision_height`, so this one names the five rows and points at it; the test is
  renamed for what it now says.

Also: HumanMale's footprint is 10 inches long and 14 wide, where 1.12 authored 12 and 10 — and
every display wearing that body answers the same pair, so the `find` over a `HashMap` above it
cannot make the assertion flap. The rest are censuses with their 5875 figures beside them: the
display table 10534 → 13452, blood tiers [36, 9903, 595] → [48, 12516, 887], breath-suppressing
models 99 → 181, footstep-shake rows 25 → 35, split-blood duplicates 8 → 11.

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 18:52:40 -06:00
co-authored by Claude Opus 5
parent e9fe05dab3
commit ca60826bec
+79 -22
View File
@@ -502,10 +502,27 @@ mod tests {
}
assert_eq!(
tiers,
[36, 9903, 595],
"tier 1 / tier 2 / tier 3 over the 10534 shipped displays"
[48, 12516, 887],
"tier 1 / tier 2 / tier 3 over the 13452 shipped displays"
);
// One short of the table, and the one is display **185**, whose `modelId` is **0** — a
// row pointing at no `CreatureModelData` row at all, which 1.12 had none of. The loop
// above skips it because there is no model to read a blood column from, and our client
// falls back the same way: no model, so nothing to draw and no blood tier to pick.
assert_eq!(
tiers.iter().sum::<usize>(),
cat.display.len() - 1,
"every display but the one with no model row is tiered"
);
assert_eq!(
cat.display
.iter()
.filter(|(id, _)| cat.model(**id).is_none())
.map(|(id, r)| (*id, r.model_id))
.collect::<Vec<_>>(),
[(185, 0)],
"display 185 is the only dangling one; a second would want its own look"
);
assert_eq!(tiers.iter().sum::<usize>(), cat.display.len());
}
/// A zero `CreatureDisplayInfo.CreatureModelScale` cannot mean "no model": the column does not
@@ -632,7 +649,7 @@ mod tests {
split.sort_unstable();
assert_eq!(
split.len(),
8,
11,
"models whose duplicate rows disagree about blood, one of them −1: {split:?}"
);
assert!(
@@ -688,8 +705,12 @@ mod tests {
.expect("some display wears the HumanMale body");
let p = cat.footprint(human).expect("HumanMale leaves prints");
assert_eq!(p.texture_id, 1, "the Base boot print");
assert!((p.length - 12.0 / 36.0).abs() < 1e-6, "length {}", p.length);
assert!((p.width - 10.0 / 36.0).abs() < 1e-6, "width {}", p.width);
// 10 inches long here, where 1.12 authored 12; the width is unchanged at 10.
assert!((p.length - 10.0 / 36.0).abs() < 1e-6, "length {}", p.length);
// 14 inches wide, where 1.12 authored 10. Consistent across every display wearing this
// body — all 30-odd of them answer the same pair — so the `find` above picking an
// arbitrary one out of a `HashMap` cannot make this flap.
assert!((p.width - 14.0 / 36.0).abs() < 1e-6, "width {}", p.width);
let printless = cat
.display
.iter()
@@ -806,7 +827,7 @@ mod tests {
);
n += 1;
}
assert_eq!(n, 10_534, "the whole shipped display table");
assert_eq!(n, 13_452, "the whole shipped display table (10534 in 5875)");
}
/// Fields 11 and 12 are `CameraShakes.dbc` ids; 25 models, all heavy, carry a footstep shake.
@@ -850,7 +871,10 @@ mod tests {
assert!(shakes.get(thud).is_some(), "{path} thud {thud} dangles");
}
}
assert_eq!(footstep, 25, "the shipped footstep-shake census");
assert_eq!(
footstep, 35,
"the shipped footstep-shake census (25 in 5875)"
);
}
#[test]
@@ -880,8 +904,8 @@ mod tests {
assert_eq!(
cat.models.values().filter(|m| m.flags & 0x2 != 0).count(),
99,
"the shipped census: 99 of {} models suppress breath",
181,
"the shipped census: 181 of {} models suppress breath (99 of 791 in 5875)",
cat.models.len()
);
for (display_id, label) in [
@@ -931,7 +955,10 @@ mod tests {
.values()
.filter(|m| m.footstep_shake != 0)
.count();
assert_eq!(shakers, 25, "footstep-shake rows (schema alignment guard)");
assert_eq!(
shakers, 35,
"footstep-shake rows (schema alignment guard; 25 in 5875)"
);
}
#[test]
@@ -977,10 +1004,30 @@ mod tests {
.unwrap_or_else(|e| panic!("{label}: parse M2: {e:#}"));
let hdr = &fmt.model().header;
let box_z = (hdr.collision_box_max[2] - hdr.collision_box_min[2]).abs();
assert!(
(box_z - h).abs() < 5e-4,
"{label}: MD20 collision box Z extent {box_z} != column {h}"
);
// In 1.12 every one of the sixteen agreed with its own art. Five do not here: the
// realm ships its own character models and the boxes moved while the DBC columns
// stayed, so OrcMale and TrollMale carry a 2.031 box (HumanMale's, a reused rig)
// under columns of 2.361 and 2.083, DwarfMale 1.607 under 1.667, TrollFemale 2.427
// under 1.839, and GnomeFemale 1.056 under a column of exactly 1.0. The model
// **paths** are unchanged, so it is the art that was replaced.
//
// The column stays authoritative, which is the part that matters: the reference reads
// it and not the model (`0x60b270` → `0x6174b0`), so the depth lines follow the
// column. An orc's swim line therefore sits where a 2.361 unit's would, a little
// above where its art suggests — the realm's own inconsistency, reproduced.
const ART_DISAGREES: [u32; 5] = [51, 53, 1478, 1479, 1564];
if ART_DISAGREES.contains(&display_id) {
assert!(
(box_z - h).abs() >= 5e-4,
"{label}: art and column agree again ({box_z} vs {h}) — drop it from \
ART_DISAGREES rather than leaving a stale exception"
);
} else {
assert!(
(box_z - h).abs() < 5e-4,
"{label}: MD20 collision box Z extent {box_z} != column {h}"
);
}
}
// Not vacuous: the races differ.
@@ -992,23 +1039,33 @@ mod tests {
);
}
/// vmangos folds both scales into `SCALE_X`, so the prism's `max(SCALE_X, display scale)` floor
/// bites only where `modelScale < 1`, and no shipped row is below 1.
/// vmangos folds both scales into `SCALE_X`, so the prism's `max(SCALE_X, display scale)`
/// floor bites only where `modelScale < 1` — and five rows are here, where 1.12 had none. So
/// the floor is exercised rather than inert, and the arithmetic it exercises has its own test
/// in `entities::collision_height`: `the_display_column_is_a_floor_never_a_multiplier`.
#[test]
fn no_shipped_model_scales_below_one_so_the_prism_floor_is_inert_at_rest() {
fn the_five_sub_one_model_scales_are_where_the_prism_floor_bites() {
let data = crate::wow_data_or_skip!();
let mut chain = crate::open_chain(&data).expect("open chain");
let cat = load_creature_catalog(&mut chain).expect("load creature catalog");
let under: Vec<_> = cat
let mut under: Vec<(u32, f32)> = cat
.models
.iter()
.filter(|(_, m)| m.scale < 1.0)
.map(|(id, m)| (*id, m.scale))
.collect();
assert!(
under.is_empty(),
"a sub-1 modelScale would make the floor bite at rest: {under:?}"
under.sort_by_key(|&(id, _)| id);
assert_eq!(
under,
[
(2602, 0.4),
(2719, 0.3),
(2720, 0.4),
(2721, 0.3),
(2722, 0.2)
],
"the models whose own scale sinks SCALE_X under the display column"
);
// Not vacuous: the column is read, and varies.
assert!(