Files
benilla-octo/crates/benilla-formats/tests/m2_uv_anim_variation.rs
T
SamandClaude Fable 5 8522c65937 the lava bubbles move again, and the bonfire keeps its wood
- Blackrock Mountain's lava bubbles animate at last: their sprite's flip-book channel was baked from a sequence they never play — UV and tint loops now bake per sequence, and a placement whose sequences disagree gets its own material
- a bonfire's logs were vanishing while its flame burned on: a body planted just under its own floor now claims the room above it, and the flame card and fire sound follow the body's cull instead of outliving it
- a WMO prop's animation host stops being cull-tested as an object in its own right
- WOW_TILE_RADIUS's doc now states its real default, 2, and why

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 17:27:28 +07:00

99 lines
4.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The BRM lava bubbles' animation is frozen (bug B98, decision 1408) — and the asset says why in
//! one number: **every one of this model's animated channels is keyed inside file sequence slot 1,
//! and slot 1 is a 50 %-weighted variation of animation id 0.**
//!
//! `BlackrockStatueLavaBubble.m2` is placed 15 times inside `Blackrock.wmo`. It authors two
//! sequences, both animation id 0 with frequencies 16384/16383 — so the reference re-rolls between
//! them every play-window (wow-re `doodad-anim-host.md` §5, decision 0768) and two placements are
//! on different slots at the same instant. Slot 0 is a dead hold: its bone-scale window is two
//! identity keys and its texture-transform window is two zero keys. Slot 1 is the whole animation:
//! the bubbles swell 1.0 → 2.785 while the sprite's V offset flips by 0.605.
//!
//! benilla baked the UV loop against slot 0 and only slot 0, through a registry keyed by MATERIAL —
//! shared by every instance of the batch, so there is no sequence to key on. On this model that
//! bakes to `None`: no registration, no table row, no scroll, on every placement, for ever. This
//! file pins both halves of that — the dead slot 0 and the live slot 1 — so the per-sequence set
//! can never silently collapse back to one loop.
//!
//! Skips (passes) when the client isn't present at `<repo>/WoW/Data`.
use benilla_formats::{open_chain, parse_m2_animations, parse_m2_render_submeshes};
const BUBBLE: &str =
"World\\KhazModan\\Blackrock\\PassiveDoodads\\BlackrockLavaBubbles\\BlackrockStatueLavaBubble.m2";
#[test]
fn the_lava_bubble_keys_its_whole_uv_flipbook_in_variation_one() {
let data = benilla_formats::wow_data_or_skip!();
let mut chain = open_chain(&data).expect("open vanilla patch chain");
let bytes = chain
.read_file(BUBBLE)
.expect("the bubble M2 is in the chain");
// 1. Two sequences, both animation id 0 — a variation chain, not a state machine. The roll is
// per placement and per window, so no single slot can be "the" one to bake.
let anims = parse_m2_animations(&bytes);
assert_eq!(anims.len(), 2, "two sequences");
assert!(
anims.iter().all(|a| a.anim_id == 0),
"both are animation id 0 — one variation chain"
);
assert!(
anims.iter().all(|a| a.frequency > 15_000),
"…and both are genuinely rollable (≈50/50), so placements DIVERGE: {:?}",
anims.iter().map(|a| a.frequency).collect::<Vec<_>>()
);
// 2. The bones say it first: slot 0 holds bind pose, slot 1 swells past 2.7×.
let peak = |slot: usize| {
anims[slot]
.bones
.iter()
.flat_map(|b| b.scale.iter().map(|&(_, s)| s[0]))
.fold(0.0f32, f32::max)
};
assert!((peak(0) - 1.0).abs() < 1e-3, "slot 0 is a rest hold");
assert!(peak(1) > 2.5, "slot 1 is the swell (peak {})", peak(1));
// 3. …and the UV channel agrees, which is the half benilla renders. Every batch carries a
// per-sequence set — the bake's own "the slots disagree, this batch cannot take the shared
// lane" verdict — whose slot 0 is empty and whose slot 1 moves.
let subs = parse_m2_render_submeshes(&bytes, "", &[]).expect("parse");
assert_eq!(subs.len(), 5, "five bubbles, five batches");
for (i, sub) in subs.iter().enumerate() {
let set = sub
.uv_seq
.as_ref()
.unwrap_or_else(|| panic!("batch {i} carries a per-sequence UV set"));
assert!(
set.uniform().is_none(),
"batch {i}: the slots disagree — that IS the verdict"
);
assert!(
set.slots()[0].is_none(),
"batch {i}: slot 0's window never moves the UVs — the dead bake"
);
let live = set.slots()[1]
.as_ref()
.unwrap_or_else(|| panic!("batch {i}: slot 1 carries the flipbook"));
let v_span = live
.keys
.iter()
.fold((f32::MAX, f32::MIN), |(lo, hi), &(_, v)| {
(lo.min(v[1]), hi.max(v[1]))
});
assert!(
v_span.1 - v_span.0 > 0.5,
"batch {i}: the V offset flips a whole sprite row (span {:?})",
v_span
);
}
// 4. And the pre-1408 read — the shared lane's slot-0 bake — is `None` on every batch. This is
// the defect stated as an assertion: nothing to register, so nothing ever scrolled.
assert!(
subs.iter().all(|s| s.uv_anim.is_none()),
"the slot-0 bake yields nothing on any batch — the frozen sprite B98 reported"
);
}