using System; using System.Collections.Generic; using System.Numerics; using AcDream.Core.Audio; using AcDream.Core.Physics; using DatReaderWriter.DBObjs; using DatReaderWriter.Types; using DRWSound = DatReaderWriter.Enums.Sound; namespace AcDream.App.Audio; /// /// that routes sound-bearing animation /// hooks (, , /// ) into the /// . /// /// /// Wiring: /// /// /// → direct play of SoundHook.Id (a /// Wave dat id) at the entity's world position. Used for custom / /// per-animation audio like weapon swoosh or spell chant. /// /// /// → look up the entity's SoundTable + /// the hook's SoundType, roll one /// via /// , play its wave. Retail's "footstep /// that varies slightly" mechanism — also how attack / damage sounds /// pick a creature-specific variant. /// /// /// → same as SoundHook but with /// pitch / volume overrides baked into the hook. /// /// /// /// /// /// Entity → SoundTable id is resolved via an /// callback passed in at construction; the renderer's per-entity state /// bag knows the PhysicsObj's SoundTableId (retail: /// PhysicsObj.soundtable_id). /// /// public sealed class AudioHookSink : IAnimationHookSink { private readonly OpenAlAudioEngine _engine; private readonly DatSoundCache _cache; private readonly IEntitySoundTable _entitySoundTables; private readonly Random _rng; public AudioHookSink( OpenAlAudioEngine engine, DatSoundCache cache, IEntitySoundTable entitySoundTables, Random? rng = null) { _engine = engine ?? throw new ArgumentNullException(nameof(engine)); _cache = cache ?? throw new ArgumentNullException(nameof(cache)); _entitySoundTables = entitySoundTables ?? throw new ArgumentNullException(nameof(entitySoundTables)); _rng = rng ?? Random.Shared; } public void OnHook(uint entityId, Vector3 entityWorldPosition, AnimationHook hook) { if (!_engine.IsAvailable) return; switch (hook) { case SoundHook s: Play(entityId, entityWorldPosition, (uint)s.Id, volume: 1f, priority: 4, pitch: 1f); break; case SoundTableHook st: PlayFromSoundTable(entityId, entityWorldPosition, st.SoundType); break; case SoundTweakedHook stw: // SoundTweakedHook is a direct wave play with volume + // priority overrides baked into the hook itself (NOT a // SoundTable lookup — that's SoundTableHook). Retail uses // this for the rare "explicit wave + explicit volume" case. Play(entityId, entityWorldPosition, waveId: (uint)stw.SoundId, volume: Math.Clamp(stw.Volume > 0 ? stw.Volume : 1f, 0f, 1f), priority: stw.Priority, pitch: 1f); break; // All the visual-only hooks (Scale, Luminous, Diffuse, …) // are for other sinks to handle. } } private void PlayFromSoundTable( uint entityId, Vector3 worldPos, DRWSound sound, float volumeMult = 1f, float pitchMult = 1f) { uint tableId = _entitySoundTables.GetSoundTableId(entityId); if (tableId == 0) return; SoundTable? table = _cache.GetSoundTable(tableId); if (table is null) return; var entry = SoundCookbook.Roll(table, sound, _rng); if (entry is null) return; Play( entityId, worldPos, waveId: (uint)entry.Id, volume: Math.Clamp(entry.Volume * volumeMult, 0f, 1f), priority: entry.Priority, pitch: Math.Max(0.5f, Math.Min(2.0f, pitchMult))); } private void Play(uint entityId, Vector3 worldPos, uint waveId, float volume, float priority, float pitch) { if (waveId == 0) return; WaveData? wave = _cache.GetWave(waveId); if (wave is null) return; _engine.Play3DWave(waveId, wave, worldPos, volume, priority, pitch); } } /// /// Callback the renderer uses to resolve the sound-table id for an /// entity. Retail stores this on PhysicsObj.soundtable_id; our /// renderer keeps per-entity state that includes it. /// public interface IEntitySoundTable { /// /// Return the SoundTable dat id (0x20xxxxxx) for , /// or 0 if no table is known (e.g. a static prop without audio). /// uint GetSoundTableId(uint entityId); } /// /// Simple dictionary-backed ; the renderer /// assigns entries as it hydrates entities. /// public sealed class DictionaryEntitySoundTable : IEntitySoundTable { private readonly Dictionary _table = new(); public void Set(uint entityId, uint soundTableId) => _table[entityId] = soundTableId; public void Remove(uint entityId) => _table.Remove(entityId); public uint GetSoundTableId(uint entityId) => _table.TryGetValue(entityId, out var id) ? id : 0; }