Files
benilla-octo/crates/benilla-formats/tests/m2_lights.rs
T
SamandClaude Fable 5 2ad0043523 benilla: initial public release
A from-scratch World of Warcraft 1.12.1 client in Rust + Bevy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 14:27:01 +07:00

110 lines
4.2 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.
//! M2 light-block parse — byte-level check against a real Elwynn campfire, the dynamic point-light
//! "hot-spot" caster (decision 0016). Pins the vanilla `0xd4` record stride and the diffuse-colour track
//! offset. Skips when the client isn't present.
use std::path::PathBuf;
use benilla_formats::{parse_m2_lights, Chain};
fn vanilla_data_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../WoW/Data")
}
#[test]
fn elwynn_campfire_has_one_warm_point_light() {
let data = vanilla_data_dir();
if !data.is_dir() {
eprintln!("skipping: vanilla client not present at {}", data.display());
return;
}
let reader = Chain::open(&data).expect("open vanilla patch chain");
let bytes = reader
.read("World\\Azeroth\\Elwynn\\PassiveDoodads\\Campfire\\ElwynnCampfire.m2")
.expect("read ElwynnCampfire.m2");
let lights = parse_m2_lights(&bytes);
assert_eq!(lights.len(), 1, "campfire has exactly one M2 light");
let l = lights[0];
assert!(
l.is_point(),
"campfire light is type 1 (point), got type {}",
l.light_type
);
// The authored warm-orange diffuse pins the 0xd4 record stride AND the 0x48 diffuse-colour track
// offset (independently cross-checked ≈ (0.71, 0.20, 0.00) — a garbled stride would not land here).
let d = l.diffuse_color;
assert!(
(d[0] - 0.71).abs() < 0.03 && (d[1] - 0.20).abs() < 0.03 && d[2] < 0.05,
"campfire diffuse ≈ (0.71, 0.20, 0.00), got {d:?}"
);
}
/// The **held torch** — the light an NPC carries into the world (`Club_1H_Torch_A_01.m2`, the model
/// `ItemDisplayInfo` 12236 / item 1906 "Monster - Torch" resolves to). Pins the one asset the entity
/// carried-light path exists for: a `type==1` point light on bone 9, up the shaft, warm, `×3`
/// intensity, and — the gate — a visibility track whose first key is nonzero, so it CASTS.
#[test]
fn held_torch_casts_one_warm_point_light_up_the_shaft() {
let data = vanilla_data_dir();
if !data.is_dir() {
eprintln!("skipping: vanilla client not present at {}", data.display());
return;
}
let reader = Chain::open(&data).expect("open vanilla patch chain");
let bytes = reader
.read("Item\\ObjectComponents\\Weapon\\Club_1H_Torch_A_01.m2")
.expect("read Club_1H_Torch_A_01.m2");
let lights = parse_m2_lights(&bytes);
assert_eq!(lights.len(), 1, "the torch has exactly one M2 light");
let l = lights[0];
assert!(l.casts(), "point + visibility-on: the torch really glows");
assert_eq!(l.bone, 9, "the light hangs on the torch's own bone");
assert!(
(l.position[0] - 0.5765).abs() < 0.01 && l.position[1].abs() < 0.01,
"0.58 yd up the shaft (the head), got {:?}",
l.position
);
let d = l.diffuse_color.map(|c| c * l.diffuse_intensity);
assert!(
(d[0] - 1.40).abs() < 0.01 && (d[1] - 0.871).abs() < 0.01 && (d[2] - 0.40).abs() < 0.01,
"diffuse × intensity ≈ (1.40, 0.87, 0.40) — flame orange, over-driven, got {d:?}"
);
}
/// The **dark** shape (wow-re `m2-dynamic-lights.md` §9.4): a `type==1` light whose visibility track
/// ships a static `0` key never casts. 11 of the corpus's 85 point lights are this — mostly spell
/// impact VFX — so the gate is not academic; before it, every one of them lit the world.
#[test]
fn a_static_zero_visibility_key_reads_as_dark() {
let data = vanilla_data_dir();
if !data.is_dir() {
eprintln!("skipping: vanilla client not present at {}", data.display());
return;
}
let reader = Chain::open(&data).expect("open vanilla patch chain");
let bytes = reader
.read("Spells\\FlameStrike_ImpactDD_Med_Base.m2")
.expect("read FlameStrike_ImpactDD_Med_Base.m2");
let dark: Vec<_> = parse_m2_lights(&bytes)
.into_iter()
.filter(|l| l.is_point())
.collect();
assert!(
!dark.is_empty(),
"the impact model does author point lights"
);
assert!(
dark.iter().all(|l| l.visibility_off && !l.casts()),
"every one is held dark by its visibility track"
);
}
#[test]
fn too_short_yields_no_lights() {
// No MD20 lights array header → empty, no panic.
assert!(parse_m2_lights(&[0u8; 16]).is_empty());
}