// MIT License - Copyright (c) 2023 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Core.Random { using System; using System.Runtime.Serialization; using System.Text.Json.Serialization; using Helper; using ProtoBuf; /// /// A reimplementation of legacy .NET System.Random behavior for deterministic, serializable use. /// /// /// /// Mirrors the algorithm used by .NET's classic System.Random so its sequence can be serialized and /// reproduced across platforms. This is not the same as .NET 6+ Random improvements and is slower than /// modern PRNGs. /// /// Pros: /// /// Behavioral parity with classic System.Random sequences for compatibility. /// Deterministic and serializable via . /// /// Cons: /// /// Slower than PCG/Xoroshiro/RomuDuo; larger state. /// Not cryptographically secure; modest statistical quality. /// /// When to use: /// /// When you need to match or migrate code that relied on System.Random. /// /// When not to use: /// /// General gameplay—prefer faster, higher-quality PRNGs like PCG or IllusionFlow (via ). /// /// /// /// /// using WallstopStudios.UnityHelpers.Core.Random; /// /// var compatible = new SystemRandom(seed: 123); /// // Produces the same sequence as classic System.Random initialized with 123 /// int a = compatible.Next(); /// double d = compatible.NextDouble(); /// /// [RandomGeneratorMetadata( RandomQuality.Poor, "Thin wrapper over System.Random; inherits same LCG weaknesses and fails modern statistical batteries.", "System.Random considered harmful", "https://nullprogram.com/blog/2017/09/21/" )] [Serializable] [DataContract] [ProtoContract(SkipConstructor = true)] public sealed class SystemRandom : AbstractRandom { private const int HalfwayInt = int.MaxValue / 2; private const int SeedArraySize = 56; private const int LastSeedIndex = SeedArraySize - 1; public static SystemRandom Instance => ThreadLocalRandom.Instance; public override RandomState InternalState => BuildState( unchecked((ulong)_inext), unchecked((ulong)_inextp), ArrayConverter.IntArrayToByteArrayBlockCopy(_seedArray) ); /* Copied from Random.cs source. Apparently it isn't guaranteed to be the same across platforms, a fact which defeats the purpose of these serializable randoms. */ [ProtoMember(6)] private int _inext; [ProtoMember(7)] private int _inextp; [ProtoMember(8)] private readonly int[] _seedArray = new int[SeedArraySize]; public SystemRandom() : this(Guid.NewGuid().GetHashCode()) { } public SystemRandom(int seed) { int num1 = 161803398 - (seed == int.MinValue ? int.MaxValue : Math.Abs(seed)); _seedArray[LastSeedIndex] = num1; int num2 = 1; for (int index1 = 1; index1 < LastSeedIndex; ++index1) { int index2 = 21 * index1 % LastSeedIndex; _seedArray[index2] = num2; num2 = num1 - num2; if (num2 < 0) { num2 += int.MaxValue; } num1 = _seedArray[index2]; } for (int index3 = 1; index3 < 5; ++index3) { for (int index4 = 1; index4 < SeedArraySize; ++index4) { int value = _seedArray[index4] -= _seedArray[1 + (index4 + 30) % LastSeedIndex]; if (value < 0) { _seedArray[index4] += int.MaxValue; } } } _inext = 0; _inextp = 21; } [JsonConstructor] public SystemRandom(RandomState internalState) { unchecked { _inext = (int)internalState.State1; _inextp = (int)internalState.State2; } RestoreCommonState(internalState); _seedArray = ArrayConverter.ByteArrayToIntArrayBlockCopy(internalState._payload); } public override int Next() { int localINext = _inext; int localINextP = _inextp; int index1; if ((index1 = localINext + 1) >= SeedArraySize) { index1 = 1; } int index2; if ((index2 = localINextP + 1) >= SeedArraySize) { index2 = 1; } int num = _seedArray[index1] - _seedArray[index2]; if (num == int.MaxValue) { --num; } if (num < 0) { num += int.MaxValue; } _seedArray[index1] = num; _inext = index1; _inextp = index2; return num; } public override uint NextUint() { if (NextBool()) { return unchecked((uint)(Next() ^ 0x80000000)); } return unchecked((uint)Next()); } public override bool NextBool() { return Next() < HalfwayInt; } public override double NextDouble() { double random; do { random = Next() / (1.0 * int.MaxValue); } while (1.0 <= random); return random; } public override float NextFloat() { float random; do { random = Next() * (1f / int.MaxValue); } while (1f <= random); return random; } public override IRandom Copy() { SystemRandom copy = new(InternalState); Array.Copy(_seedArray, copy._seedArray, _seedArray.Length); return copy; } } }