Part of #41: the comments of `benilla-formats` (the library, its extract tools, examples and tests) are rewritten to the rule in `docs/METHOD.md`, which is what the code does and the reference fact behind it, in a line. 19,654 comment lines become 7,264. Fixes #105: the WMO collision comments now name walking's box-query leaf `0x6bca50` and the camera and line-of-sight segment-query leaf `0x6bc700`, in `models/collision.rs`, `examples/wmo_bsp.rs` and `tests/chain.rs`. - **The mechanical strip.** The citations that were only a private record number go first, and each edit is shown to be a pure removal. - **Wrong facts corrected.** The rewrite corrects the comments that stated wrong facts. Among them: - A placed doodad's material alpha reads file slot 0 on its spawn clock, while the reference reads bone 0's armed sequence, re-rolled every play window. The old text said doodads are armed once at load. - The fog end is stored ×36 and scaled once at load (`0x6d6090`). It is not raw. - The MCSH bit picks a map doodad's sun scale, 1.0 lit or 0.5 shadowed. 2.5 belongs to an entity's light node. - `SpellRec` is the row as stored, column × 4, not a translated copy. - A global sequence's clock is anchored at the instance's attach time (`0x714352`). - Particle twinkle is a gated size multiplier (`0x7b2adc`). - The blob shadow sizes from the Stand box. - Data claims are checked against the install: the weapon-trail kits' parameters and alphas, the beam rows' noise, and the maps' `field_16`. - Dozens more turned up on the way: renamed or deleted items, docs sitting on the wrong item, stale counts and links. - **Deviation labels.** "Deviation:" now marks only a choice with a reason. An unbuilt behaviour says so plainly. - **Verification.** Every file strips to identical code, and `cargo fmt --check` is clean. Four second readers compared every file old against new, restored the facts the rewrite had dropped, and checked each new citation against the reference, the code and the shipped data. String literals are untouched; they are #92. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
107 lines
4.0 KiB
Rust
107 lines
4.0 KiB
Rust
//! A GameObject's idle is animation id 0 through its `playableAnimationLookup` (`0x71019b`), not
|
|
//! its first sequence. The book authors the door family in the order Close (146), Closed, Open,
|
|
//! Opened, all looping (`flags` bit 0 clear, `0x714585`), and its table sends id 0 to 147 Closed,
|
|
//! a still shut book; its first sequence is the 0.333 s Close sweep.
|
|
|
|
use benilla_formats::{
|
|
open_chain, parse_m2_animation_lookup, parse_m2_animations, parse_m2_playable_animation_lookup,
|
|
};
|
|
|
|
const BOOK: &str = "World\\Goober\\G_BookOpenMediumBrown.m2";
|
|
/// `AnimationData.dbc`: 146 Close (motion), 147 Closed (rest), 148 Open (motion), 149 Opened (rest).
|
|
const CLOSE: u16 = 146;
|
|
const CLOSED: u16 = 147;
|
|
const OPEN: u16 = 148;
|
|
|
|
#[test]
|
|
fn the_book_idle_resolves_to_closed_not_the_close_motion() {
|
|
let data = benilla_formats::wow_data_or_skip!();
|
|
let mut chain = open_chain(&data).expect("open vanilla patch chain");
|
|
let bytes = chain.read_file(BOOK).expect("the book M2 is in the chain");
|
|
let anims = parse_m2_animations(&bytes);
|
|
|
|
assert_eq!(
|
|
anims.first().map(|a| a.anim_id),
|
|
Some(CLOSE),
|
|
"the model's FIRST sequence is the Close motion, not a rest pose — the whole point"
|
|
);
|
|
|
|
assert!(
|
|
anims[0].looping,
|
|
"Close is a looping band — arming it is what made the book cycle for ever"
|
|
);
|
|
|
|
let playable = parse_m2_playable_animation_lookup(&bytes).expect("playable lookup");
|
|
let idle_id = playable.first().map_or(0, |p| p.resolved_id);
|
|
assert_eq!(
|
|
idle_id, CLOSED,
|
|
"playableAnimationLookup[0] resolves to Closed — the seed the reference arms"
|
|
);
|
|
|
|
// Bone 0 turns about x: x ≈ 0 is shut, x ≈ 0.69 (87°) open. Closed's band (533..633 ms) has no
|
|
// keys of its own, so the window rule makes the identity keys at 500 and 667 one constant.
|
|
let root_rot = |id: u16| -> Vec<[f32; 4]> {
|
|
anims
|
|
.iter()
|
|
.find(|a| a.anim_id == id)
|
|
.and_then(|a| a.bones.iter().find(|b| b.bone == 0))
|
|
.map(|b| b.rotation.iter().map(|(_, q)| *q).collect())
|
|
.unwrap_or_default()
|
|
};
|
|
let closed = root_rot(CLOSED);
|
|
assert!(!closed.is_empty(), "Closed must resolve to a pose");
|
|
for q in &closed {
|
|
assert!(
|
|
q[0].abs() < 1e-3,
|
|
"Closed must hold the SHUT pose (bone 0 x ≈ 0), got {}",
|
|
q[0]
|
|
);
|
|
}
|
|
|
|
let close_motion = root_rot(CLOSE);
|
|
let (lo, hi) = close_motion
|
|
.iter()
|
|
.fold((f32::MAX, f32::MIN), |(lo, hi), q| {
|
|
(lo.min(q[0]), hi.max(q[0]))
|
|
});
|
|
assert!(
|
|
hi - lo > 0.5 && lo.abs() < 1e-3 && hi > 0.5,
|
|
"Close must swing from open ({hi}) to shut ({lo}) — that sweep, looped, IS the reported bug"
|
|
);
|
|
|
|
// Closed tilts its two cover bones 2.5° off bind, which keeps the book on the skinned path.
|
|
let closed_seq = anims
|
|
.iter()
|
|
.find(|a| a.anim_id == CLOSED)
|
|
.expect("the book authors Closed");
|
|
let tilted = closed_seq
|
|
.bones
|
|
.iter()
|
|
.filter(|b| {
|
|
b.rotation
|
|
.iter()
|
|
.any(|(_, q)| (q[3].abs() - 1.0).abs() > 1e-4)
|
|
})
|
|
.count();
|
|
assert!(
|
|
tilted >= 2,
|
|
"Closed must pose bones off bind (the covers), got {tilted}"
|
|
);
|
|
|
|
// The ownership table the missing-sequence remap branches on (`0x711960`): the door family.
|
|
let lookup = parse_m2_animation_lookup(&bytes).expect("animation lookup");
|
|
let owns = |id: u16| lookup.get(id as usize).is_some_and(|&s| s != 0xffff);
|
|
for id in [CLOSE, CLOSED, OPEN, 149] {
|
|
assert!(owns(id), "the book authors animation id {id}");
|
|
}
|
|
assert!(
|
|
!owns(0),
|
|
"the book authors NO Stand — the seed reaches Closed only through the playable table, \
|
|
which is exactly why a naive `find(Stand)` would arm nothing"
|
|
);
|
|
assert!(
|
|
!owns(150),
|
|
"ids past the table's end read as the out-of-bounds sentinel, i.e. not owned"
|
|
);
|
|
}
|