- Twenty of the game's forty-three maps are not made of terrain at all — Molten Core, Onyxia's Lair, Dire Maul, Gnomeregan, Uldaman, the Deeprun Tram and both Blackrocks among them. Their whole world is one building named by the map file, and that line was being thrown away, so those maps loaded nothing and a teleport in fell forever. The building is now placed as an ordinary WMO placement — so collision, props, liquid, interior lighting and portals all come along on the existing path — verified on seven of the twenty against the server's own independent floor heights. - Particles keep the model's time. Emitter rate and on/off now bake per animation sequence and follow the clip that is actually playing, so a quest object's explosion choreography no longer burns at full roar while the object idles. And the flipbook cell ramp is computed the way the reference computes it: sheets that play backwards no longer crash the client (a certain Winterspring brazier fired this on every approach), the tail streak reads its own ramp instead of the head's, and the per-segment repeat count works — the Insect Swarm's wings finally flap. - A culled dungeon room takes its furniture with it: lanterns, crates, barrels and cobwebs now cull with the WMO group that owns them, instead of hanging lit in the void where their room used to be. The group's own liquid goes along too. - Under the floor: the archive chain pins that a file the patch deletes can never fall through to the stale copy in a base archive — a silent failure mode that would serve deleted content looking perfectly healthy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
65 lines
2.5 KiB
Rust
65 lines
2.5 KiB
Rust
//! Print a model's particle-emitter records — the authored numbers to hold a live
|
||
//! `WOW_PARTICLE_CENSUS` line against: `cargo run -p benilla-formats --example dump_emitters --
|
||
//! 'World\...\RubyCrystalLarge01.m2'`.
|
||
//!
|
||
//! The pair is the check that matters (decision 0653): the census says how many particles are
|
||
//! *live*, this says how many the file asks for (`rate × lifespan`), and the over-life ramp says
|
||
//! how big they get. A mismatch is our sim's bug; a match moves the question to the look.
|
||
//!
|
||
//! Output is Blizzard data — pipe it to the scratchpad, never into the repo.
|
||
|
||
fn main() -> anyhow::Result<()> {
|
||
let virt = std::env::args()
|
||
.nth(1)
|
||
.ok_or_else(|| anyhow::anyhow!("usage: dump_emitters <m2 path>"))?;
|
||
let data = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../WoW/Data");
|
||
let mut chain = benilla_formats::open_chain(&data)?;
|
||
let bytes = chain.read_file(&virt)?;
|
||
for (i, e) in benilla_formats::parse_m2_particle_emitters(&bytes)?
|
||
.iter()
|
||
.enumerate()
|
||
{
|
||
println!(
|
||
"emitter[{i}] flags {:#x} bone {} shape {:?} blend {:?}",
|
||
e.flags, e.bone, e.shape, e.blend
|
||
);
|
||
println!(
|
||
" texture {:?} tiles {}x{} head_tail {}",
|
||
e.texture, e.tile_rows, e.tile_cols, e.head_tail
|
||
);
|
||
println!(
|
||
" speed {} var {} vrange {} hrange {} gravity {} life {} drag {}",
|
||
e.emission_speed,
|
||
e.speed_variation,
|
||
e.vertical_range,
|
||
e.horizontal_range,
|
||
e.gravity,
|
||
e.lifespan,
|
||
e.drag
|
||
);
|
||
match e.timing.constant_rate() {
|
||
Some(r) => println!(" rate {r}/s (constant, every sequence)"),
|
||
None => {
|
||
for (slot, (looping, rate, enabled)) in e.timing.slot_views().iter().enumerate() {
|
||
println!(
|
||
" seq {slot}{}: rate {:?} enabled {:?}",
|
||
if *looping { "" } else { " (clamped)" },
|
||
rate,
|
||
enabled
|
||
);
|
||
}
|
||
}
|
||
}
|
||
println!(
|
||
" area {}x{} zsrc {} tail {} spin {}",
|
||
e.area_length, e.area_width, e.z_source, e.tail_time, e.spin
|
||
);
|
||
println!(
|
||
" twinkle speed {} pct {} min {} max {}",
|
||
e.twinkle_speed, e.twinkle_percent, e.twinkle_min, e.twinkle_max
|
||
);
|
||
println!(" over_life {:?}", e.over_life);
|
||
}
|
||
Ok(())
|
||
}
|