Files
benilla-octo/crates/benilla-formats/src/weapon_swing.rs
T
SamandClaude Fable 5 a8ab442d27 every melee hit makes a sound, and footsteps get their armor foley
- every connecting melee swing makes a sound: the reference splits its swing sound in two — a miss whoosh by handedness, and for a landed hit, parry, block, immune or deflect the WeaponSwingSounds2 kit picked by the swinging hand's weight class, a DBC column (ItemSubClass.WeaponSwingSize) this crate had carried unread; deflect and absorb get their real kits after three weeks as silent stubs
- a footfall is two sounds: the terrain step and the armor foley of your chest piece's material — chain, plate and leather rustle, cloth is silent by data; on the real 1.12.1 files every creature model's foley material is 0, so the foley you hear in the reference is your own, and that is what ships
- the paper doll's turn cross-fades at both ends instead of cutting — the reference plays every turn clip blended over the incoming clip's own authored blend time (0.25 s into a shuffle, 0.5 s back to Stand on HumanMale), and the outgoing clip keeps running under the fade rather than freezing, which corrects a "universal snapshot law" an earlier record had over-generalised; `m2seq` prints the blend column that settles such questions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 18:31:38 +07:00

113 lines
4.9 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.
//! `WeaponSwingSounds2.dbc` — the *connecting* melee swing's whoosh, by weapon weight.
//!
//! The reference builds a flat six-slot cache out of this file at load
//! (`0x45cb00`: a reverse walk over the 16-byte records writing
//! `cache[(critical != 0) + swingType*2] = SoundEntriesID`, bounded `swingType < 3` and
//! `critical < 2`), and the play site indexes it with exactly that arithmetic
//! (`0x457f8d lea eax,[eax+ecx*2]`, `0x457f91 mov edx,[eax*4 + 0xb06bd4]`). This catalog is that
//! cache, built the same way, so a file whose rows moved would still land where the reference
//! puts them.
//!
//! Layout — VERIFIED against the shipped build 5875 file (6 records × 4 fields × 16 B, one-byte
//! string block): `ID(0), SwingType(1), Critical(2), SoundEntriesID(3)`. The six rows are the
//! full cross product, and the kits they name are `LightWeaponNormal/Critical` (233/234),
//! `MediumWeaponNormal/Critical` (235/236) and `HeavyWeaponNormal/Critical` (237/238) — the
//! `mWooshSmall*` / `mWooshMedium*` / `mWooshLarge*` samples.
//!
//! **The weight is not a heuristic**: `swingType` is `ItemSubClass.WeaponSwingSize` for the
//! swinging weapon's `(class 2, subclass)` — see [`crate::ItemSubClassCatalog::weapon_swing_size`].
use anyhow::{Context, Result};
use benilla_dbc::{FieldType, Schema, SchemaField};
use crate::dbc::{parse, u32_at};
use crate::Chain;
/// How many `swingType` values the reference's cache holds — `0x457f63 cmp ecx,3 / jge` bails
/// above this, and the loader's own `cmp edi,3 / jge` refuses to write past it.
const SWING_TYPES: usize = 3;
/// The six-slot swing-kit cache (`0xb06bd4`), indexed `critical + swingType*2`.
pub struct WeaponSwingCatalog {
cache: [u32; SWING_TYPES * 2],
}
impl WeaponSwingCatalog {
/// The kit for a swing of `swing_type` (0 light · 1 medium · 2 heavy), critical or not.
/// `None` above the reference's ceiling — its play site *returns without playing anything*
/// on `swingType >= 3`, so an out-of-range weight is silence, never a fallback to light.
pub fn kit(&self, swing_type: u32, critical: bool) -> Option<u32> {
let slot = usize::try_from(swing_type).ok()?;
if slot >= SWING_TYPES {
return None;
}
let kit = self.cache[usize::from(critical) + slot * 2];
(kit != 0).then_some(kit)
}
}
fn schema() -> Schema {
let mut s = Schema::new("WeaponSwingSounds2");
for name in ["ID", "SwingType", "Critical", "SoundEntriesID"] {
s.add_field(SchemaField::new(name, FieldType::UInt32));
}
s
}
/// Read `WeaponSwingSounds2.dbc` off the patch chain into the reference's cache shape.
pub fn load_weapon_swing_catalog(chain: &mut Chain) -> Result<WeaponSwingCatalog> {
let bytes = chain
.read_file("DBFilesClient\\WeaponSwingSounds2.dbc")
.context("reading WeaponSwingSounds2.dbc")?;
let rs = parse(&bytes, schema(), "WeaponSwingSounds2")?;
let mut cache = [0u32; SWING_TYPES * 2];
for r in rs.records() {
let g = |i: usize| u32_at(r, i).unwrap_or(0);
let (swing_type, critical) = (g(1), g(2));
// The loader's own bounds, kept verbatim: a row outside them is dropped, not clamped.
if swing_type as usize >= SWING_TYPES || critical >= 2 {
continue;
}
cache[(critical != 0) as usize + swing_type as usize * 2] = g(3);
}
Ok(WeaponSwingCatalog { cache })
}
#[cfg(test)]
mod tests {
use super::*;
/// The shipped table: the full 3 × 2 cross product, on the six `mWoosh*` kits. Skips without
/// client data.
#[test]
fn real_weapon_swing_sounds_decode() {
let data = crate::wow_data_or_skip!();
let mut chain = crate::open_chain(&data).expect("open chain");
let cat = load_weapon_swing_catalog(&mut chain).expect("load weapon swing sounds");
assert_eq!(cat.kit(0, false), Some(233), "LightWeaponNormal");
assert_eq!(cat.kit(0, true), Some(234), "LightWeaponCritical");
assert_eq!(cat.kit(1, false), Some(235), "MediumWeaponNormal");
assert_eq!(cat.kit(1, true), Some(236), "MediumWeaponCritical");
assert_eq!(cat.kit(2, false), Some(237), "HeavyWeaponNormal");
assert_eq!(cat.kit(2, true), Some(238), "HeavyWeaponCritical");
}
/// Above the reference's `swingType < 3` ceiling there is no sound at all — `0x457f60`
/// returns before touching the cache. Nothing to skip: the bound is ours, not the file's.
#[test]
fn a_weight_past_the_ceiling_is_silence_not_a_fallback() {
let cat = WeaponSwingCatalog {
cache: [233, 234, 235, 236, 237, 238],
};
assert_eq!(cat.kit(3, false), None);
assert_eq!(cat.kit(u32::MAX, true), None);
// …and a slot the file never filled reads as absent rather than kit 0.
let sparse = WeaponSwingCatalog {
cache: [233, 0, 0, 0, 0, 0],
};
assert_eq!(sparse.kit(0, false), Some(233));
assert_eq!(sparse.kit(0, true), None);
}
}