creature: a display that names a model is drawn, whatever its scale column says
A screenshot of the Gnomeregan Reclamation Facility (OctoWoW's own area 5211) with its NPCs replaced by flat grey boxes, and the target frame filled — `M. C. Volterwhite`, 57, 4.13k — behind an empty portrait. The boxes are `entities::attach`'s model-less debug cube, and the empty portrait is the same cause: the display resolved to nothing to draw. `new_creature_display` took a row only `if m.scale > 0.0`, where `scale` is `CreatureModelData.ModelScale × CreatureDisplayInfo.CreatureModelScale`. That column does not scale a spawned unit: the server folds it into `OBJECT_FIELD_SCALE_X`, which is why `Creatures::model_scale` says in its own doc that the glue screens' pet is its only reader. So the gate was testing a number nothing downstream consults, and a `0.0` in it meant "no model". His chain's `CreatureDisplayInfo.dbc` (the effective copy, `patch-A.mpq` — patches sort ascending by lowercase name, so A wins over 5) carries a contiguous custom block, **20256-20356, 100 rows, every one a character model**: 27 HumanMale, 16 HumanFemale, 16 TrollMale, 12 GnomeMale, 10 NightElfFemale and the rest, all authored with that column left at `0.0`. One batch of custom NPCs, one zone's worth of grey boxes. Stock 1.12 ships three of its own that way too (displays 16, 17 and 19: HumanThief, HUMLBlackSmith, HUMLMerchant), which is the proof the column was never a visibility switch. The gate is gone. The one shape left that earns a cube — a display id with no row in either DBC — now says so once per id, as the GameObject leg already does, so the next zone full of cubes names the ids instead of needing a screenshot. A test over the real chain pins the data: every zero-scale row still names a real model, and the only row that answers `None` does so for want of a `CreatureModelData` row (display 185, `ModelID = 0`), not for its scale. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -191,21 +191,30 @@ impl DisplayModel {
|
||||
}
|
||||
|
||||
/// Whether the display names a model file. An empty `parts` from a named model is that model's
|
||||
/// answer, draw nothing; [`empty_display`] (no catalog row, a zero-scale row, no model path) is
|
||||
/// answer, draw nothing; [`empty_display`] (no catalog row, no model path) is
|
||||
/// an unresolved display, and earns the debug cube.
|
||||
pub(super) fn names_a_model(&self) -> bool {
|
||||
!matches!(self.handle, ModelHandle::None)
|
||||
}
|
||||
}
|
||||
|
||||
/// A creature display's M2, its skins filled at build; a missing or zero-scale row is empty.
|
||||
/// A creature display's M2, its skins filled at build; only a display with no row is empty.
|
||||
///
|
||||
/// It does **not** gate on `CreatureDisplayInfo.CreatureModelScale`. That column does not scale a
|
||||
/// spawned unit here or in the reference — the server folds it into `OBJECT_FIELD_SCALE_X`
|
||||
/// (see [`super::Creatures::model_scale`], whose only reader is the glue screens' pet), so a unit
|
||||
/// is drawn at the size the server sent and the column is never consulted. Gating on it cost this
|
||||
/// chain a contiguous block of **100 custom display rows, 20256-20356**, every one a character
|
||||
/// model (27 HumanMale, 16 HumanFemale, 16 TrollMale, 12 GnomeMale…) authored with the column left
|
||||
/// at `0.0` — a whole zone's NPCs, each one a red debug cube with an empty portrait instead of a
|
||||
/// body. A row that names a model names a model.
|
||||
pub(super) fn new_creature_display(
|
||||
catalog: &CreatureCatalog,
|
||||
display_id: u32,
|
||||
asset_server: &AssetServer,
|
||||
) -> DisplayModel {
|
||||
match catalog.model(display_id) {
|
||||
Some(m) if m.scale > 0.0 => DisplayModel {
|
||||
Some(m) => DisplayModel {
|
||||
handle: ModelHandle::M2(asset_server.load(m2_url(&m.model_path))),
|
||||
dir: model_dir(&m.model_path).to_string(),
|
||||
skins: m.textures,
|
||||
@@ -216,7 +225,17 @@ pub(super) fn new_creature_display(
|
||||
.is_some_and(|p| p.eq_ignore_ascii_case("character\\")),
|
||||
..empty_shell()
|
||||
},
|
||||
_ => empty_display(),
|
||||
// The one way left for a unit to end up as the red debug cube, said once per display id
|
||||
// (a display is built once and cached). The cube is a gap of ours — the reference draws
|
||||
// the model, or nothing, but never a box — so this line is what names the display the
|
||||
// server sent that this chain's DBCs cannot answer for.
|
||||
None => {
|
||||
info!(
|
||||
"creature display {display_id} has no row in this chain's \
|
||||
CreatureDisplayInfo/CreatureModelData — nothing to draw but the debug cube"
|
||||
);
|
||||
empty_display()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -508,6 +508,58 @@ mod tests {
|
||||
assert_eq!(tiers.iter().sum::<usize>(), cat.display.len());
|
||||
}
|
||||
|
||||
/// A zero `CreatureDisplayInfo.CreatureModelScale` cannot mean "no model": the column does not
|
||||
/// scale a spawned unit at all — the server folds it into `OBJECT_FIELD_SCALE_X`, and the only
|
||||
/// reader on this side is the glue screens' pet — so rows carrying a `0.0` still name a real
|
||||
/// model and must still be drawn.
|
||||
///
|
||||
/// Stock 1.12 ships three of them (displays 16, 17 and 19: HumanThief, HUMLBlackSmith,
|
||||
/// HUMLMerchant), and OctoWoW's chain adds a contiguous custom block, 20256-20356, every row a
|
||||
/// character model. Gating a display on this column turned that whole block into red debug
|
||||
/// cubes with empty portraits — a zone's worth of NPCs.
|
||||
#[test]
|
||||
fn a_zero_display_scale_still_names_a_real_model() {
|
||||
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 zero: Vec<u32> = cat
|
||||
.display
|
||||
.iter()
|
||||
.filter(|(_, row)| row.scale == 0.0)
|
||||
.map(|(id, _)| *id)
|
||||
.collect();
|
||||
assert!(
|
||||
!zero.is_empty(),
|
||||
"every chain carries some: stock 1.12 alone has displays 16, 17 and 19"
|
||||
);
|
||||
// A row whose `ModelID` names no `CreatureModelData` row is a different shape, and the
|
||||
// only one that earns the debug cube: the chain carries exactly one, display 185 with
|
||||
// `ModelID = 0`.
|
||||
let (no_model, named): (Vec<u32>, Vec<u32>) =
|
||||
zero.iter().partition(|id| cat.model(**id).is_none());
|
||||
for id in &no_model {
|
||||
let row = &cat.display[id];
|
||||
assert!(
|
||||
!cat.models.contains_key(&row.model_id),
|
||||
"display {id} answers None only for want of CreatureModelData row {}",
|
||||
row.model_id
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
!named.is_empty(),
|
||||
"every chain carries some: stock 1.12 alone has displays 16, 17 and 19"
|
||||
);
|
||||
for id in &named {
|
||||
let m = cat.model(*id).expect("partitioned as resolvable");
|
||||
assert!(
|
||||
!m.model_path.trim().is_empty(),
|
||||
"display {id} carries a zero scale but still names {:?}",
|
||||
m.model_path
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Tier 3 is ordinary fauna, so its records base cannot mean no blood.
|
||||
#[test]
|
||||
fn the_tier_three_fallback_bleeds_red_on_ordinary_creatures() {
|
||||
|
||||
Reference in New Issue
Block a user