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!(