// MIT License - Copyright (c) 2024 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Core.Random { using System; using System.Buffers.Binary; using System.Collections.Generic; using System.Reflection; using System.Runtime.Serialization; using System.Text.Json.Serialization; using Helper; using ProtoBuf; /// /// A thin wrapper around System.Random that exposes the API and supports state capture. /// /// /// /// Uses a real System.Random internally and advances it to reflect the captured . /// This makes it easy to interop with code that expects System.Random semantics while using the unified /// interface. /// /// Pros: /// /// Compatibility with System.Random behavior. /// Unified API; determinism via state capture. /// /// Cons: /// /// Slower than modern PRNGs; not cryptographically secure. /// Internal advance required after deserialization to sync to generation count. /// /// When to use: /// /// Bridging code that uses System.Random to the ecosystem. /// /// When not to use: /// /// Performance-critical or quality-sensitive randomness—prefer PCG or IllusionFlow. /// /// /// /// /// using WallstopStudios.UnityHelpers.Core.Random; /// /// var compat = new DotNetRandom(Guid.NewGuid()); /// // Use IRandom methods while maintaining System.Random semantics /// byte b = compat.NextByte(); /// float f = compat.NextFloat(); /// /// [RandomGeneratorMetadata( RandomQuality.Poor, "Linear congruential generator (mod 2^31) with known correlation failures; unsuitable for high-quality simulations.", "System.Random considered harmful", "https://nullprogram.com/blog/2017/09/21/" )] [Serializable] [DataContract] [ProtoContract(SkipConstructor = true)] public sealed class DotNetRandom : AbstractRandom { private const BindingFlags RandomFieldFlags = BindingFlags.Instance | BindingFlags.NonPublic; private static readonly FieldInfo SeedArrayField = typeof(Random).GetField("SeedArray", RandomFieldFlags) ?? typeof(Random).GetField("_seedArray", RandomFieldFlags); private static readonly FieldInfo InextField = typeof(Random).GetField("inext", RandomFieldFlags) ?? typeof(Random).GetField("_inext", RandomFieldFlags); private static readonly FieldInfo InextpField = typeof(Random).GetField("inextp", RandomFieldFlags) ?? typeof(Random).GetField("_inextp", RandomFieldFlags); private static readonly Func SeedArrayGetter = TryCreateGetter( SeedArrayField ); private static readonly FieldSetter SeedArraySetter = TryCreateSetter( SeedArrayField ); private static readonly Func InextGetter = TryCreateGetter(InextField); private static readonly FieldSetter InextSetter = TryCreateSetter( InextField ); private static readonly Func InextpGetter = TryCreateGetter(InextpField); private static readonly FieldSetter InextpSetter = TryCreateSetter( InextpField ); private static readonly bool SnapshotSupported = SeedArrayField != null && InextField != null && InextpField != null; public static DotNetRandom Instance => ThreadLocalRandom.Instance; public override RandomState InternalState => BuildState( unchecked((ulong)_seed), state2: _numberGenerated, payload: CaptureSerializedState() ); [ProtoMember(6)] private ulong _numberGenerated; [ProtoMember(7)] private int _seed; [ProtoMember(8)] [JsonInclude] private byte[] SerializedState { get => CaptureSerializedState(); set => _pendingStatePayload = value; } [ProtoIgnore] [JsonIgnore] private byte[] _pendingStatePayload; private Random _random; public DotNetRandom() : this(Guid.NewGuid()) { } public DotNetRandom(Guid guid) { // Derive a deterministic 32-bit seed from GUID bytes without allocations _seed = RandomUtilities.GuidToInt32(guid); _random = new Random(_seed); _pendingStatePayload = null; } [JsonConstructor] public DotNetRandom(RandomState internalState) { _seed = unchecked((int)internalState.State1); _numberGenerated = internalState.State2; _pendingStatePayload = CopyPayload(internalState.PayloadBytes); RestoreCommonState(internalState); EnsureRandomInitialized(); } [ProtoAfterDeserialization] private void OnProtoDeserialize() { EnsureRandomInitialized(); } private void EnsureRandomInitialized() { if (_random != null) { return; } _random = new Random(_seed); if (_pendingStatePayload != null) { if ( TryDeserializeSnapshot(_pendingStatePayload, out RandomSnapshot snapshot) && TryApplySnapshot(_random, snapshot) ) { _pendingStatePayload = null; return; } // Snapshot could not be applied (e.g., runtime no longer exposes the fields); // fall back to deterministic replay and drop the stale payload. _pendingStatePayload = null; } for (ulong i = 0; i < _numberGenerated; ++i) { _ = _random.Next(int.MinValue, int.MaxValue); } } public override uint NextUint() { EnsureRandomInitialized(); ++_numberGenerated; return unchecked((uint)_random.Next(int.MinValue, int.MaxValue)); } public override IRandom Copy() { return new DotNetRandom(InternalState); } private byte[] CaptureSerializedState() { EnsureRandomInitialized(); if (!TryCaptureSnapshot(_random, out RandomSnapshot snapshot)) { return null; } return SerializeSnapshot(snapshot); } private static byte[] CopyPayload(IReadOnlyList payload) { if (payload == null || payload.Count == 0) { return null; } byte[] buffer = new byte[payload.Count]; for (int i = 0; i < payload.Count; ++i) { buffer[i] = payload[i]; } return buffer; } private static bool TryCaptureSnapshot(Random random, out RandomSnapshot snapshot) { snapshot = default; if (!SnapshotSupported || random == null) { return false; } try { int[] seedArray = SeedArrayGetter != null ? SeedArrayGetter(random) : (int[])SeedArrayField.GetValue(random); if (seedArray == null) { return false; } int[] seedClone = new int[seedArray.Length]; Array.Copy(seedArray, seedClone, seedClone.Length); int inext = InextGetter != null ? InextGetter(random) : (int)InextField.GetValue(random); int inextp = InextpGetter != null ? InextpGetter(random) : (int)InextpField.GetValue(random); snapshot = new RandomSnapshot(inext, inextp, seedClone); return true; } catch { return false; } } private static Func TryCreateGetter(FieldInfo field) { if (field == null) { return null; } try { return ReflectionHelpers.GetFieldGetter(field); } catch { return null; } } private static FieldSetter TryCreateSetter(FieldInfo field) { if (field == null) { return null; } try { return ReflectionHelpers.GetFieldSetter(field); } catch { return null; } } private static bool TryApplySnapshot(Random random, RandomSnapshot snapshot) { if (!SnapshotSupported || random == null || snapshot.SeedArray == null) { return false; } try { int[] seedClone = new int[snapshot.SeedArray.Length]; Array.Copy(snapshot.SeedArray, seedClone, seedClone.Length); bool seedApplied = false; if (SeedArraySetter != null) { Random instance = random; SeedArraySetter(ref instance, seedClone); seedApplied = true; } else { SeedArrayField.SetValue(random, seedClone); seedApplied = true; } if (!seedApplied) { return false; } if (InextSetter != null) { Random instance = random; InextSetter(ref instance, snapshot.Inext); } else { InextField.SetValue(random, snapshot.Inext); } if (InextpSetter != null) { Random instance = random; InextpSetter(ref instance, snapshot.Inextp); } else { InextpField.SetValue(random, snapshot.Inextp); } return true; } catch { return false; } } private static byte[] SerializeSnapshot(RandomSnapshot snapshot) { if (snapshot.SeedArray == null) { return null; } int length = snapshot.SeedArray.Length; byte[] buffer = new byte[12 + length * sizeof(int)]; Span span = buffer.AsSpan(); BinaryPrimitives.WriteInt32LittleEndian(span, length); BinaryPrimitives.WriteInt32LittleEndian(span.Slice(4), snapshot.Inext); BinaryPrimitives.WriteInt32LittleEndian(span.Slice(8), snapshot.Inextp); Span seedSpan = span.Slice(12); for (int i = 0; i < length; ++i) { BinaryPrimitives.WriteInt32LittleEndian( seedSpan.Slice(i * sizeof(int)), snapshot.SeedArray[i] ); } return buffer; } private static bool TryDeserializeSnapshot( IReadOnlyList payload, out RandomSnapshot snapshot ) { snapshot = default; if (payload == null || payload.Count < 12) { return false; } Span header = stackalloc byte[12]; for (int i = 0; i < 12; ++i) { header[i] = payload[i]; } int length = BinaryPrimitives.ReadInt32LittleEndian(header); if (length <= 0) { return false; } int expectedBytes = 12 + length * sizeof(int); if (payload.Count < expectedBytes) { return false; } int inext = BinaryPrimitives.ReadInt32LittleEndian(header.Slice(4)); int inextp = BinaryPrimitives.ReadInt32LittleEndian(header.Slice(8)); int[] seedArray = new int[length]; for (int i = 0; i < length; ++i) { Span temp = stackalloc byte[sizeof(int)]; int offset = 12 + i * sizeof(int); for (int j = 0; j < sizeof(int); ++j) { temp[j] = payload[offset + j]; } seedArray[i] = BinaryPrimitives.ReadInt32LittleEndian(temp); } snapshot = new RandomSnapshot(inext, inextp, seedArray); return true; } private readonly struct RandomSnapshot { public RandomSnapshot(int inext, int inextp, int[] seedArray) { Inext = inext; Inextp = inextp; SeedArray = seedArray; } public int Inext { get; } public int Inextp { get; } public int[] SeedArray { get; } } } }