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;
}