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 indexRandom::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); }