From ca60826bec0e6ef32724b4cbaba32255317277d6 Mon Sep 17 00:00:00 2001 From: grom Date: Sat, 31 Oct 2026 18:52:40 -0600 Subject: [PATCH] The creature tables, and three things in them worth more than a count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn --- crates/benilla-formats/src/creatures.rs | 101 ++++++++++++++++++------ 1 file changed, 79 insertions(+), 22 deletions(-) diff --git a/crates/benilla-formats/src/creatures.rs b/crates/benilla-formats/src/creatures.rs index db6d80af..817f794f 100644 --- a/crates/benilla-formats/src/creatures.rs +++ b/crates/benilla-formats/src/creatures.rs @@ -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::(), + 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::>(), + [(185, 0)], + "display 185 is the only dangling one; a second would want its own look" ); - assert_eq!(tiers.iter().sum::(), 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!(