using System;
namespace AcDream.Core.Audio;
///
/// The two random sources retail's sound code actually draws from. They are
/// NOT interchangeable, and the difference is observable at small
/// probabilities, so the seam models both rather than collapsing them.
///
///
/// Variant index — Random::RollDice(0, 1) @ 0x0042C4C0,
/// a dual-LCG returning a float hard-clamped to 0.99999988
/// (0x00797D48), i.e. it can never return 1.0. That clamp is what
/// makes the last variant of a multi-entry sound unreachable — see
/// .
///
///
///
/// Probability gate — C library rand() scaled by
/// 1/32767 inside SoundManager::PlayProbability @
/// 0x005500E0. rand() returns 0..32767 INCLUSIVE, so the
/// scaled value reaches exactly 1.0 and the grid step is ~3.05e-5. Two
/// consequences we reproduce deliberately: a probability of 1.0 is skipped
/// on the single roll where the value lands on 1.0 (1 chance in 32768), and
/// the six authored 0.0001 probabilities in the shipped dats resolve to
/// ~1.2e-4 rather than 1e-4 because only rand() <= 3 passes.
///
///
public interface ISoundRandom
{
///
/// Retail Random::RollDice(0, 1): a float in
/// [0, 0.99999988], never 1.0.
///
float NextVariantRoll();
///
/// Retail rand() * (1/32767): a float in [0, 1] inclusive,
/// quantised to the 1/32767 grid.
///
float NextProbabilityRoll();
}
///
/// over . Retail's exact LCG
/// streams are not reproduced — nothing observable depends on the sequence,
/// only on each roll's range and quantisation, which this preserves.
///
public sealed class SoundRandom : ISoundRandom
{
///
/// Retail's clamp on Random::rand (0x00797D48). Equals
/// 1 - 2^-23, the largest float below 1.0.
///
internal const float MaxVariantRoll = 0.99999988f;
/// C's RAND_MAX; retail divides by exactly this.
internal const int RandMax = 32767;
private readonly Random _rng;
public SoundRandom(Random? rng = null) => _rng = rng ?? Random.Shared;
public float NextVariantRoll() =>
MathF.Min(MaxVariantRoll, (float)_rng.NextDouble());
// Next's upper bound is exclusive, so RandMax + 1 makes RAND_MAX itself
// reachable — which is what lets the scaled roll reach exactly 1.0.
public float NextProbabilityRoll() =>
_rng.Next(0, RandMax + 1) * (1f / RandMax);
}