using System;
using System.Collections.Generic;
using DatReaderWriter.DBObjs;
using DRWSound = DatReaderWriter.Enums.Sound;
using DRWSoundEntry = DatReaderWriter.Types.SoundEntry;
namespace AcDream.Core.Audio;
///
/// Retail's sound-selection model over a . Two
/// INDEPENDENT steps, in retail's own order — a uniform variant pick, then a
/// Bernoulli play/skip gate on the picked entry.
///
///
/// Step 1 — . SoundManager::GetSound @
/// 0x00550680: idx = (int)(roll * (n - 1)), truncating toward
/// zero, where roll is .
/// The entry's Probability plays NO part in selection.
///
///
///
/// Step 2 — . SoundManager::PlayProbability
/// @ 0x005500E0: rand() * (1/32767) < probability. Failing the
/// gate means the sound is simply not played — there is no fallback entry and
/// no retry.
///
///
///
/// Corrects a pre-2026-08-08 divergence. The previous implementation
/// walked a cumulative distribution weighted BY probability and
/// short-circuited single-entry lists before rolling at all. The shipped dats
/// hold 4,184 entries of which 4,183 are single-entry and 686 of those carry
/// a probability below 1.0, so the gate was categorically absent: retail's
/// 5%-chance creature idle chatter (Speak1, 49 entries at 0.05) fired
/// on every trigger, wound/attack/swoosh variants never dropped, and six
/// 0.0001-probability easter eggs played every time. Census and
/// disassembly: docs/research/2026-08-08-audio-retail-dat-layer.md §2.
///
///
public static class SoundCookbook
{
///
/// Retail's uniform variant pick. Returns null only for an empty list.
///
///
/// The n - 1 is retail's, at 0x005506C8, and it is a real
/// Turbine off-by-one: because the roll is clamped below 1.0, the index
/// never reaches n - 1 and the LAST entry of a multi-entry sound
/// can never be selected. It is reproduced verbatim per the
/// port-faithfully rule ("do not 'fix' the decompiled code"). Blast
/// radius in the shipped dats is exactly one wave: only
/// 0x200000A8 / SoundType 31 has two entries, so
/// 0x0A00051E is retail-unreachable. Changing this to n
/// would need a divergence-register row.
///
///
public static DRWSoundEntry? PickVariant(
IReadOnlyList entries,
ISoundRandom rng)
{
ArgumentNullException.ThrowIfNull(entries);
ArgumentNullException.ThrowIfNull(rng);
if (entries.Count == 0) return null;
int idx = (int)(rng.NextVariantRoll() * (entries.Count - 1));
// Retail bounds-checks here too (`cmp eax, esi; jae return`); the
// branch is dead given the roll clamp, but it costs nothing to keep
// the same shape.
return idx < entries.Count ? entries[idx] : null;
}
///
/// Retail's Bernoulli play gate. True = play, false = silence.
///
public static bool PlayProbability(float probability, ISoundRandom rng)
{
ArgumentNullException.ThrowIfNull(rng);
return rng.NextProbabilityRoll() < probability;
}
///
/// The two steps composed the way every retail play site composes them:
/// pick a variant, then gate it. Returns null when the sound is missing,
/// its variant list is empty, or the gate says silence.
///
public static DRWSoundEntry? Select(
IReadOnlyList entries,
ISoundRandom rng)
{
DRWSoundEntry? picked = PickVariant(entries, rng);
if (picked is null) return null;
return PlayProbability(picked.Probability, rng) ? picked : null;
}
///
/// with
/// the table lookup in front: given a and a
/// retail slot, resolve the entry to play.
///
public static DRWSoundEntry? Select(
SoundTable table,
DRWSound sound,
ISoundRandom rng)
{
ArgumentNullException.ThrowIfNull(table);
if (!table.Sounds.TryGetValue(sound, out var soundData)) return null;
return Select(soundData.Entries, rng);
}
}