Files
benilla-octo/crates/benilla-formats/src/item_sounds.rs
T
SamandClaude Fable 5 6b980b9efb the player build: one binary that carries its content
- a player build ships beside the dev one — dev chords and probe surfaces go dark, and a gate enforces what a player can still reach, by medium not by module
- the binary carries its own shaders and its shipped UI, finds the WoW folder by itself, and keeps its local state in one benilla-config folder
- the glue screens get a capture shutter, and the engine stops depending on the protocol crate

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

128 lines
5.3 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.
//! `ItemGroupSounds.dbc` adapter — the per-item **pickup / put-down / use** sound groups
//! (decision 0091: the bag-drag item sounds).
//!
//! Layout — VERIFIED byte-exact against build 5875 (wow-re
//! `system/sound/scratch/item-pickup-place-sound.md`, §5 cross-checked): **5 fields × 4 = 20 B**
//! per record (loader `0x5477d0` asserts fieldCount 5 @`0x547879`, recordSize 0x14 @`0x5478ae`):
//! `{ id, kit[0], kit[1], kit[2], kit[3] }` — the kits are `SoundEntries.dbc` ids, indexed by the
//! client's **gesture**: `kit[0]` pickup/grab, `kit[1]` put-down/place, `kit[2]` use/activate
//! (`0x458024: mov ecx,[eax+4*ecx+0x4]`); `kit[3]` is unused by any caller. 24 rows in the real
//! DBC. An item reaches its group through `ItemDisplayInfo.field11`
//! ([`crate::items::ItemDisplay::group_sounds`]); a `0` kit slot means the gesture is silent —
//! the client's play tail drops kit id 0.
use std::collections::HashMap;
use crate::Chain;
use anyhow::{Context, Result};
use benilla_dbc::{FieldType, Schema, SchemaField};
use crate::dbc::{parse, u32_at};
const ITEM_GROUP_SOUNDS: &str = "DBFilesClient\\ItemGroupSounds.dbc";
/// The client's gesture index into an [`ItemGroupSoundsCatalog`] row (the `ecx` every
/// `SndInterfacePlayItemSound` caller passes — wow-re `item-pickup-place-sound.md`).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ItemGesture {
/// `ecx = 0` — item grabbed onto the cursor.
Pickup = 0,
/// `ecx = 1` — item placed / put down / cursor cleared.
PutDown = 1,
/// `ecx = 2` — item used/activated (only groups 1–6 populate it).
Use = 2,
}
/// `ItemGroupSounds.dbc`, keyed by group id (`ItemDisplayInfo.field11`).
pub struct ItemGroupSoundsCatalog {
groups: HashMap<u32, [u32; 4]>,
}
impl ItemGroupSoundsCatalog {
/// The `SoundEntries` kit for a group's gesture — `None` when the group id is unknown or the
/// slot is `0` (both are the client's silent returns: the bounds/null checks at
/// `0x45800f`/`0x45801d`, and the play tail dropping kit 0).
pub fn kit(&self, group: u32, gesture: ItemGesture) -> Option<u32> {
self.groups
.get(&group)
.map(|kits| kits[gesture as usize])
.filter(|&k| k != 0)
}
pub fn len(&self) -> usize {
self.groups.len()
}
pub fn is_empty(&self) -> bool {
self.groups.is_empty()
}
}
fn item_group_sounds_schema() -> Schema {
let mut s = Schema::new("ItemGroupSounds");
for name in ["ID", "Pickup", "PutDown", "Use", "Unused3"] {
s.add_field(SchemaField::new(name, FieldType::UInt32));
}
s
}
/// Load `ItemGroupSounds.dbc` off the patch chain.
pub fn load_item_group_sounds(chain: &mut Chain) -> Result<ItemGroupSoundsCatalog> {
let bytes = chain
.read_file(ITEM_GROUP_SOUNDS)
.with_context(|| format!("reading {ITEM_GROUP_SOUNDS}"))?;
let rs = parse(&bytes, item_group_sounds_schema(), "ItemGroupSounds")?;
let mut groups = HashMap::with_capacity(rs.records().len());
for r in rs.records() {
let Some(id) = u32_at(r, 0) else { continue };
groups.insert(id, std::array::from_fn(|i| u32_at(r, 1 + i).unwrap_or(0)));
}
Ok(ItemGroupSoundsCatalog { groups })
}
#[cfg(test)]
mod tests {
use super::*;
/// The real 5875 rows match the RE's corroboration decode (`item-pickup-place-sound.md`):
/// 24 groups; id 1 → kits [273, 274, 275, 0] (a group with a use kit), id 7 → [1185, 1202, 0, 0]
/// (a weapon/armor group, no use kit — its `Use` gesture resolves silent).
#[test]
fn real_item_group_sounds_resolve() {
let data = crate::wow_data_or_skip!();
let mut chain = crate::open_chain(&data).expect("open chain");
let cat = load_item_group_sounds(&mut chain).expect("load ItemGroupSounds");
assert_eq!(cat.len(), 24, "24 groups in build 5875");
assert_eq!(cat.kit(1, ItemGesture::Pickup), Some(273));
assert_eq!(cat.kit(1, ItemGesture::PutDown), Some(274));
assert_eq!(cat.kit(1, ItemGesture::Use), Some(275));
assert_eq!(cat.kit(7, ItemGesture::Pickup), Some(1185));
assert_eq!(cat.kit(7, ItemGesture::PutDown), Some(1202));
assert_eq!(cat.kit(7, ItemGesture::Use), None, "a 0 slot is silent");
assert_eq!(cat.kit(999, ItemGesture::Pickup), None, "unknown group");
}
/// The display→group→kit join holds on real data: every nonzero `ItemDisplayInfo.field11`
/// (`group_sounds`) is a valid group id — the RE's 20513/20513 corroboration, re-run through
/// our own two adapters.
#[test]
fn real_display_group_ids_all_resolve() {
let data = crate::wow_data_or_skip!();
let mut chain = crate::open_chain(&data).expect("open chain");
let sounds = load_item_group_sounds(&mut chain).expect("load ItemGroupSounds");
let displays = crate::load_item_display_catalog(&mut chain).expect("load ItemDisplayInfo");
let mut nonzero = 0usize;
let mut valid = 0usize;
for d in displays.iter() {
if d.group_sounds != 0 {
nonzero += 1;
if sounds.groups.contains_key(&d.group_sounds) {
valid += 1;
}
}
}
assert_eq!(nonzero, 20513, "the RE's nonzero field-11 count");
assert_eq!(valid, nonzero, "every nonzero group id resolves");
}
}