// 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.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using DataStructure.Adapters; using ProtoBuf; using UnityEngine; using Utils; #if !SINGLE_THREADED using System.Collections.Concurrent; #else using Extension; #endif /// /// Common abstract base for all implementations with protobuf support. /// /// /// /// This type is annotated with [ProtoContract] and explicitly lists all known concrete /// implementations via [ProtoInclude]. This enables polymorphic protobuf serialization when /// the declared type is AbstractRandom (or another abstract base that carries the /// [ProtoInclude] annotations). /// /// /// Adding a new PRNG: implement , derive from , /// and add a new [ProtoInclude(tag, typeof(YourRandom))] entry here with a unique, stable /// field number. Never renumber existing tags once published. /// /// /// /// // 1) Implement your generator /// [ProtoContract] /// public sealed class MyCustomRandom : AbstractRandom { /* state + [ProtoMember]s... */ } /// /// // 2) Add a ProtoInclude tag below, e.g. /// // [ProtoInclude(112, typeof(MyCustomRandom))] /// /// // 3) Use AbstractRandom as your declared type in protobuf models for seamless polymorphism /// [ProtoContract] /// class RNGHolder { [ProtoMember(1)] public AbstractRandom rng; } /// /// /// /// Interfaces: protobuf-net cannot infer a concrete type when deserializing to an interface such as /// . You can still serialize an IRandom value (we use the runtime type), but for /// deserialization you must either: /// /// /// Declare the field/property as AbstractRandom (recommended), or /// Call Serializer.RegisterProtobufRoot<IRandom, SomeConcreteRandom>() at startup and /// then use Serializer.ProtoDeserialize<IRandom>. /// /// [Serializable] [DataContract] [ProtoContract] [ProtoInclude(100, typeof(DotNetRandom))] [ProtoInclude(101, typeof(PcgRandom))] [ProtoInclude(102, typeof(XorShiftRandom))] [ProtoInclude(103, typeof(WyRandom))] [ProtoInclude(104, typeof(XoroShiroRandom))] [ProtoInclude(105, typeof(UnityRandom))] [ProtoInclude(106, typeof(SystemRandom))] [ProtoInclude(107, typeof(LinearCongruentialGenerator))] [ProtoInclude(108, typeof(SquirrelRandom))] [ProtoInclude(109, typeof(RomuDuo))] [ProtoInclude(110, typeof(SplitMix64))] [ProtoInclude(111, typeof(IllusionFlow))] [ProtoInclude(112, typeof(FlurryBurstRandom))] [ProtoInclude(113, typeof(PhotonSpinRandom))] [ProtoInclude(114, typeof(StormDropRandom))] [ProtoInclude(115, typeof(BlastCircuitRandom))] [ProtoInclude(116, typeof(WaveSplatRandom))] public abstract class AbstractRandom : IRandom { #if SINGLE_THREADED private static readonly Dictionary EnumTypeCache = new(); #else private static readonly ConcurrentDictionary EnumTypeCache = new(); #endif protected const float MagicFloat = 5.960465E-008F; private const ulong LongBias = 1UL << 63; private const int MaxRejectionAttempts32 = 1 << 16; private const int MaxRejectionAttempts64 = 1 << 20; private const int MaxGaussianAttempts = 1 << 20; private const int MaxDoubleBitAttempts = 1 << 20; [ProtoMember(1)] protected double? _cachedGaussian; public abstract RandomState InternalState { get; } private readonly byte[] _guidBytes = new byte[16]; // Bit/byte reservoirs to accelerate small requests // Note: included in protobuf to preserve exact generator state across round-trips [ProtoMember(2)] protected uint _bitBuffer; [ProtoMember(3)] protected int _bitCount; [ProtoMember(4)] protected uint _byteBuffer; [ProtoMember(5)] protected int _byteCount; [MethodImpl(MethodImplOptions.AggressiveInlining)] protected RandomState BuildState( ulong state1, ulong state2 = 0, IReadOnlyList payload = null, uint? auxiliaryUintA = null, uint? auxiliaryUintB = null ) { byte[] payloadCopy = null; if (payload != null) { if (payload is byte[] payloadArray) { int length = payloadArray.Length; payloadCopy = new byte[length]; Buffer.BlockCopy(payloadArray, 0, payloadCopy, 0, length); } else { int count = payload.Count; payloadCopy = new byte[count]; for (int i = 0; i < count; ++i) { payloadCopy[i] = payload[i]; } } } return new RandomState( state1, state2, _cachedGaussian, payload: payloadCopy, bitBuffer: _bitBuffer, bitCount: _bitCount, byteBuffer: _byteBuffer, byteCount: _byteCount ); } [MethodImpl(MethodImplOptions.AggressiveInlining)] protected void RestoreCommonState(RandomState state) { _cachedGaussian = state.Gaussian; _bitBuffer = state.BitBuffer; _bitCount = state.BitCount; _byteBuffer = state.ByteBuffer; _byteCount = state.ByteCount; } public virtual int Next() { // Mask out the MSB to ensure the value is within [0, int.MaxValue] return unchecked((int)NextUint() & 0x7FFFFFFF); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public int Next(int max) { if (max <= 0) { throw new ArgumentException($"Max {max} cannot be less-than or equal-to 0"); } return unchecked((int)NextUint(unchecked((uint)max))); } public int Next(int min, int max) { if (max <= min) { throw new ArgumentException( $"Min {min} cannot be larger-than or equal-to max {max}" ); } uint range = (uint)(max - min); if (range == 0) { return unchecked((int)NextUint()); } return unchecked((int)(min + NextUint(range))); } // Internal sampler public abstract uint NextUint(); [MethodImpl(MethodImplOptions.AggressiveInlining)] public uint NextUint(uint max) { if (max == 0) { throw new ArgumentException("Max cannot be zero"); } // Power-of-two fast path if ((max & (max - 1)) == 0) { return NextUint() & (max - 1); } // Lemire's method (32-bit): take high 32 bits of r*max uint r = NextUint(); ulong m = (ulong)r * max; uint lo = (uint)m; if (lo < max) { uint t = unchecked((0u - max) % max); int attempts = 0; while (lo < t) { if (++attempts > MaxRejectionAttempts32) { // Prevent infinite loop: fall back to modulo (small bias) rather than hang return r % max; } r = NextUint(); m = (ulong)r * max; lo = (uint)m; } } return (uint)(m >> 32); } public uint NextUint(uint min, uint max) { if (max <= min) { throw new ArgumentException( $"Min {min} cannot be larger-than or equal-to max {max}" ); } return min + NextUint(max - min); } public short NextShort() { return NextShort(short.MaxValue); } public short NextShort(short max) { return NextShort(0, max); } public short NextShort(short min, short max) { return unchecked((short)Next(min, max)); } public byte NextByte() { if (_byteCount == 0) { _byteBuffer = NextUint(); _byteCount = 4; } byte b = (byte)_byteBuffer; _byteBuffer >>= 8; _byteCount--; return b; } public byte NextByte(byte max) { return NextByte(0, max); } public byte NextByte(byte min, byte max) { return unchecked((byte)Next(min, max)); } public long NextLong() { uint upper = NextUint(); uint lower = NextUint(); unchecked { return (long)((((ulong)upper << 32) | lower) & 0x7FFFFFFFFFFFFFFF); } } public long NextLong(long max) { if (max <= 0) { throw new ArgumentException($"Max {max} cannot be less-than or equal-to 0"); } return unchecked((long)NextUlong(unchecked((ulong)max))); } public long NextLong(long min, long max) { if (max <= min) { throw new ArgumentException( $"Min {min} cannot be larger-than or equal-to Max {max}" ); } ulong biasedMin = BiasLong(min); ulong biasedMax = BiasLong(max); ulong range = biasedMax - biasedMin; ulong sample = NextUlong(range); ulong biasedResult = biasedMin + sample; return UnbiasLong(biasedResult); } public virtual ulong NextUlong() { uint upper = NextUint(); uint lower = NextUint(); return ((ulong)upper << 32) | lower; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public ulong NextUlong(ulong max) { if (max == 0) { throw new ArgumentException("Max cannot be zero"); } // Power-of-two fast path (no rejection required) if ((max & (max - 1)) == 0) { return NextUlong() & (max - 1); } // Lemire's method with rejection: use high 64 bits of the 128-bit product, // retrying when the low bits fall within the threshold region to avoid bias. ulong threshold = unchecked(0UL - max) % max; int attempts = 0; while (true) { ulong sample = NextUlong(); ulong productLow = unchecked(sample * max); if (productLow >= threshold) { return MulHi64(sample, max); } if (++attempts > MaxRejectionAttempts64) { // Failsafe: fall back to modulo to avoid hanging indefinitely. return sample % max; } } } public ulong NextUlong(ulong min, ulong max) { if (max <= min) { throw new ArgumentException( $"Min {min} cannot be larger-than or equal-to max {max}" ); } return NextUlong(max - min) + min; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual bool NextBool() { if (_bitCount == 0) { _bitBuffer = NextUint(); _bitCount = 32; } bool bit = (_bitBuffer & 1u) == 0; _bitBuffer >>= 1; _bitCount--; return bit; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual void NextBytes(byte[] buffer) { if (buffer == null) { throw new ArgumentNullException(nameof(buffer)); } NextBytes(buffer.AsSpan()); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual void NextBytes(Span buffer) { int i = 0; int len = buffer.Length; // 16-byte unrolled blocks for (; i <= len - 16; i += 16) { uint r0 = NextUint(); uint r1 = NextUint(); uint r2 = NextUint(); uint r3 = NextUint(); buffer[i + 0] = (byte)r0; buffer[i + 1] = (byte)(r0 >> 8); buffer[i + 2] = (byte)(r0 >> 16); buffer[i + 3] = (byte)(r0 >> 24); buffer[i + 4] = (byte)r1; buffer[i + 5] = (byte)(r1 >> 8); buffer[i + 6] = (byte)(r1 >> 16); buffer[i + 7] = (byte)(r1 >> 24); buffer[i + 8] = (byte)r2; buffer[i + 9] = (byte)(r2 >> 8); buffer[i + 10] = (byte)(r2 >> 16); buffer[i + 11] = (byte)(r2 >> 24); buffer[i + 12] = (byte)r3; buffer[i + 13] = (byte)(r3 >> 8); buffer[i + 14] = (byte)(r3 >> 16); buffer[i + 15] = (byte)(r3 >> 24); } // 4-byte chunks for (; i <= len - 4; i += 4) { uint r = NextUint(); buffer[i + 0] = (byte)r; buffer[i + 1] = (byte)(r >> 8); buffer[i + 2] = (byte)(r >> 16); buffer[i + 3] = (byte)(r >> 24); } // Tail if (i < len) { uint r = NextUint(); int j = 0; for (; i < len; ++i, ++j) { buffer[i] = (byte)(r >> (j * 8)); } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual double NextDouble() { // 53 random bits from a 64-bit sample const double scale = 1.0 / 9007199254740992.0; // 2^53 ulong combined = NextUlong() >> 11; return combined * scale; } public double NextDouble(double max) { if (max <= 0) { throw new ArgumentException($"Max {max} cannot be less-than or equal-to 0"); } return NextDouble() * max; } public double NextDouble(double min, double max) { if (max <= min) { throw new ArgumentException( $"Min {min} cannot be larger-than or equal-to max {max}" ); } double range = max - min; if (double.IsInfinity(range)) { return NextDoubleWithInfiniteRange(min, max); } return min + NextDouble() * range; } protected double NextDoubleWithInfiniteRange(double min, double max) { ulong orderedMin = ToOrderedDouble(min); ulong orderedMax = ToOrderedDouble(max); if (orderedMax <= orderedMin) { throw new ArgumentException( $"Invalid range [{min}, {max}) for infinite-range sampling." ); } ulong range = orderedMax - orderedMin; int attempts = 0; while (true) { ulong sample = orderedMin + NextUlong(range); double value = FromOrderedDouble(sample); if (!double.IsNaN(value) && !double.IsInfinity(value)) { return value; } if (++attempts > MaxRejectionAttempts64) { // Transparent fallback: pick a finite value inside [min, max) if (double.IsPositiveInfinity(max)) { return FromOrderedDouble(orderedMax - 1); } if (double.IsNegativeInfinity(min)) { return FromOrderedDouble(orderedMin + 1); } ulong midpoint = orderedMin + (range >> 1); double midValue = FromOrderedDouble(midpoint); if (!double.IsNaN(midValue) && !double.IsInfinity(midValue)) { return midValue; } // Final safeguard: nudge just above min in ordered space return FromOrderedDouble(orderedMin + 1); } } } protected double NextDoubleFullRange() { const ulong exponentMask = 0x7FF0000000000000; ulong randomBits; int attempts = 0; do { randomBits = NextUlong(); if (++attempts > MaxDoubleBitAttempts) { // Force a finite value by clearing exponent bits randomBits &= ~exponentMask; break; } } while ((randomBits & exponentMask) == exponentMask); return BitConverter.Int64BitsToDouble(unchecked((long)randomBits)); } public double NextGaussian(double mean = 0, double stdDev = 1) { return mean + NextGaussianInternal() * stdDev; } private double NextGaussianInternal() { if (_cachedGaussian != null) { double gaussian = _cachedGaussian.Value; _cachedGaussian = null; return gaussian; } // https://stackoverflow.com/q/7183229/1917135 double x; double y; double square; int attempts = 0; do { x = 2 * NextDouble() - 1; y = 2 * NextDouble() - 1; square = x * x + y * y; if (++attempts > MaxGaussianAttempts) { // Fallback to Box-Muller without rejection to avoid infinite loop double u1 = NextDouble(); if (u1 <= double.Epsilon) { u1 = double.Epsilon; } double u2 = NextDouble(); double mag = Math.Sqrt(-2.0 * Math.Log(u1)); double z0 = mag * Math.Cos(2.0 * Math.PI * u2); double z1 = mag * Math.Sin(2.0 * Math.PI * u2); _cachedGaussian = z1; return z0; } } while (square is 0 or > 1); double fac = Math.Sqrt(-2 * Math.Log(square) / square); _cachedGaussian = x * fac; return y * fac; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual float NextFloat() { // Use 24 random bits for float mantissa return (NextUint() >> 8) * (1f / (1 << 24)); } public float NextFloat(float max) { if (max <= 0) { throw new ArgumentException($"{max} cannot be less-than or equal-to 0"); } return NextFloat() * max; } public float NextFloat(float min, float max) { if (max <= min) { throw new ArgumentException( $"Min {min} cannot be larger-than or equal-to max {max}" ); } float range = max - min; if (float.IsInfinity(range)) { return (float)NextDouble(min, max); } return min + NextFloat(range); } public T NextOf(IEnumerable enumerable) { if (enumerable is null) { throw new ArgumentNullException(nameof(enumerable)); } return enumerable switch { IReadOnlyList list => NextOf(list), IReadOnlyCollection collection => NextOf(collection), _ => NextFromEnumerable(enumerable), }; } public T NextOf(IReadOnlyCollection collection) { if (collection is not { Count: > 0 }) { throw new ArgumentException("Collection cannot be empty"); } if (collection is IReadOnlyList list) { return RandomOf(list); } int index = Next(collection.Count); switch (collection) { case HashSet hashSet: { int i = 0; foreach (T element in hashSet) { if (index == i++) { return element; } } throw new ArgumentException(nameof(collection)); } case SortedSet sortedSet: { int i = 0; foreach (T element in sortedSet) { if (index == i++) { return element; } } throw new ArgumentException(nameof(collection)); } case LinkedList linkedList: { int i = 0; foreach (T element in linkedList) { if (index == i++) { return element; } } throw new ArgumentException(nameof(collection)); } case Queue queue: { int i = 0; foreach (T element in queue) { if (index == i++) { return element; } } throw new ArgumentException(nameof(collection)); } case Stack stack: { int i = 0; foreach (T element in stack) { if (index == i++) { return element; } } throw new ArgumentException(nameof(collection)); } default: { int i = 0; foreach (T element in collection) { if (index == i++) { return element; } } throw new ArgumentException(nameof(collection)); } } } public T NextOf(IReadOnlyList list) { if (list is not { Count: > 0 }) { throw new ArgumentException("Collection cannot be empty", nameof(list)); } /* For small lists, it's much more efficient to simply return one of their elements instead of trying to generate a random number within bounds (which is implemented as a while(true) loop) */ return RandomOf(list); } public T NextOfParams(params T[] elements) { if (elements.Length == 0) { throw new ArgumentException(nameof(elements)); } return RandomOf(elements); } private T NextFromEnumerable(IEnumerable enumerable) { using IEnumerator enumerator = enumerable.GetEnumerator(); if (!enumerator.MoveNext()) { throw new ArgumentException("Collection cannot be empty", nameof(enumerable)); } T selection = enumerator.Current; ulong seen = 1; while (enumerator.MoveNext()) { seen++; if (NextUlong(seen) == 0) { selection = enumerator.Current; } } return selection; } private static T[] GetEnumValues() where T : unmanaged, Enum { Type enumType = typeof(T); Array boxedValues = EnumTypeCache.GetOrAdd(enumType, type => Enum.GetValues(type)); return Unsafe.As(ref boxedValues); } private static void EnsureEnumHasAvailableValues( T[] enumValues, ReadOnlySpan exclusions ) where T : unmanaged, Enum { if (enumValues.Length == 0) { throw new InvalidOperationException( $"Enum {typeof(T).Name} does not define any values." ); } if (exclusions.IsEmpty) { return; } int enumCount = enumValues.Length; const int StackThreshold = 8; if (exclusions.Length <= StackThreshold) { Span unique = stackalloc T[StackThreshold]; int uniqueCount = 0; foreach (T exclusion in exclusions) { if (Array.IndexOf(enumValues, exclusion) < 0) { continue; } bool seen = false; for (int i = 0; i < uniqueCount; ++i) { if (EqualityComparer.Default.Equals(unique[i], exclusion)) { seen = true; break; } } if (seen) { continue; } unique[uniqueCount++] = exclusion; if (uniqueCount >= enumCount) { ThrowAllEnumValuesExcluded(enumCount); } } return; } using PooledResource> pooledSet = Buffers.HashSet.Get(out HashSet set); foreach (T exclusion in exclusions) { if (Array.IndexOf(enumValues, exclusion) < 0) { continue; } set.Add(exclusion); if (set.Count >= enumCount) { ThrowAllEnumValuesExcluded(enumCount); } } } private T NextEnumExceptInternal(ReadOnlySpan exclusions) where T : unmanaged, Enum { T[] enumValues = GetEnumValues(); EnsureEnumHasAvailableValues(enumValues, exclusions); return SelectEnumValue(enumValues, exclusions); } private T SelectEnumValue(T[] enumValues, ReadOnlySpan exclusions) where T : unmanaged, Enum { if (exclusions.IsEmpty) { return RandomOf(enumValues); } const int StackThreshold = 128; int enumCount = enumValues.Length; Span buffer = enumCount <= StackThreshold ? stackalloc T[enumCount] : default; if (!buffer.IsEmpty) { int count = PopulateAllowedValues(enumValues, exclusions, buffer); if (count == 0) { ThrowAllEnumValuesExcluded(enumCount); } return count == 1 ? buffer[0] : buffer[Next(count)]; } using PooledArray pooled = SystemArrayPool.Get(enumCount, out T[] temp); Span tempSpan = temp.AsSpan(0, enumCount); int index = PopulateAllowedValues(enumValues, exclusions, tempSpan); if (index == 0) { ThrowAllEnumValuesExcluded(enumCount); } return index == 1 ? temp[0] : temp[Next(index)]; } private T SelectEnumValue(T[] enumValues, HashSet exclusions) where T : unmanaged, Enum { if (exclusions == null || exclusions.Count == 0) { return RandomOf(enumValues); } const int StackThreshold = 128; int enumCount = enumValues.Length; Span buffer = enumCount <= StackThreshold ? stackalloc T[enumCount] : default; if (!buffer.IsEmpty) { int count = PopulateAllowedValues(enumValues, exclusions, buffer); if (count == 0) { ThrowAllEnumValuesExcluded(enumCount); } return count == 1 ? buffer[0] : buffer[Next(count)]; } using PooledArray pooled = SystemArrayPool.Get(enumCount, out T[] temp); Span tempSpan = temp.AsSpan(0, enumCount); int index = PopulateAllowedValues(enumValues, exclusions, tempSpan); if (index == 0) { ThrowAllEnumValuesExcluded(enumCount); } return index == 1 ? temp[0] : temp[Next(index)]; } private static void EnsureEnumHasAvailableValues(T[] enumValues, HashSet exclusions) where T : struct, Enum { if (enumValues.Length == 0) { exclusions.Clear(); throw new InvalidOperationException( $"Enum {typeof(T).Name} does not define any values." ); } if (exclusions.Count == 0) { return; } int excludedCount = 0; foreach (T value in enumValues) { if (exclusions.Contains(value)) { excludedCount++; } } if (excludedCount >= enumValues.Length) { exclusions.Clear(); ThrowAllEnumValuesExcluded(enumValues.Length); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool SpanContains(ReadOnlySpan span, T value) { for (int i = 0; i < span.Length; ++i) { if (EqualityComparer.Default.Equals(span[i], value)) { return true; } } return false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int PopulateAllowedValues( T[] enumValues, ReadOnlySpan exclusions, Span destination ) where T : unmanaged, Enum { int count = 0; foreach (T value in enumValues) { if (!SpanContains(exclusions, value)) { destination[count++] = value; } } return count; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int PopulateAllowedValues( T[] enumValues, HashSet exclusions, Span destination ) where T : unmanaged, Enum { int count = 0; foreach (T value in enumValues) { if (!exclusions.Contains(value)) { destination[count++] = value; } } return count; } [MethodImpl(MethodImplOptions.NoInlining)] private static void ThrowAllEnumValuesExcluded(int enumCount) where T : struct, Enum { throw new InvalidOperationException( $"Cannot select a value from enum {typeof(T).Name} because all {enumCount} defined values are excluded." ); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ulong BiasLong(long value) { return unchecked((ulong)value + LongBias); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static long UnbiasLong(ulong value) { return unchecked((long)(value - LongBias)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ulong MulHi64(ulong x, ulong y) { #if NET7_0_OR_GREATER if (System.Runtime.Intrinsics.X86.Bmi2.X64.IsSupported) { unsafe { ulong lo; ulong hi = System.Runtime.Intrinsics.X86.Bmi2.X64.MultiplyNoFlags(x, y, &lo); return hi; } } #endif ulong x0 = (uint)x; ulong x1 = x >> 32; ulong y0 = (uint)y; ulong y1 = y >> 32; ulong p11 = x1 * y1; ulong p01 = x0 * y1; ulong p10 = x1 * y0; ulong p00 = x0 * y0; ulong middle = p10 + (p00 >> 32) + (uint)p01; ulong hi = p11 + (middle >> 32) + (p01 >> 32); return hi; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ulong ToOrderedDouble(double value) { if (double.IsNaN(value)) { throw new ArgumentException( "NaN is not a valid bound for random sampling.", nameof(value) ); } ulong bits = unchecked((ulong)BitConverter.DoubleToInt64Bits(value)); const ulong signBit = 1UL << 63; return (bits & signBit) != 0 ? ~bits : bits | signBit; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double FromOrderedDouble(ulong value) { const ulong signBit = 1UL << 63; ulong bits = (value & signBit) != 0 ? value & ~signBit : ~value; return BitConverter.Int64BitsToDouble(unchecked((long)bits)); } public T NextEnum() where T : unmanaged, Enum { T[] enumValues = GetEnumValues(); if (enumValues.Length == 0) { throw new InvalidOperationException( $"Enum {typeof(T).Name} does not define any values." ); } return RandomOf(enumValues); } public T NextEnumExcept(T exception1) where T : unmanaged, Enum { Span exclusions = stackalloc T[1]; exclusions[0] = exception1; return NextEnumExceptInternal(exclusions); } public T NextEnumExcept(T exception1, T exception2) where T : unmanaged, Enum { Span exclusions = stackalloc T[2]; exclusions[0] = exception1; exclusions[1] = exception2; return NextEnumExceptInternal(exclusions); } public T NextEnumExcept(T exception1, T exception2, T exception3) where T : unmanaged, Enum { Span exclusions = stackalloc T[3]; exclusions[0] = exception1; exclusions[1] = exception2; exclusions[2] = exception3; return NextEnumExceptInternal(exclusions); } public T NextEnumExcept(T exception1, T exception2, T exception3, T exception4) where T : unmanaged, Enum { Span exclusions = stackalloc T[4]; exclusions[0] = exception1; exclusions[1] = exception2; exclusions[2] = exception3; exclusions[3] = exception4; return NextEnumExceptInternal(exclusions); } public T NextEnumExcept( T exception1, T exception2, T exception3, T exception4, params T[] exceptions ) where T : unmanaged, Enum { T[] enumValues = GetEnumValues(); using PooledResource> bufferResource = Buffers.HashSet.Get( out HashSet set ); set.Add(exception1); set.Add(exception2); set.Add(exception3); set.Add(exception4); foreach (T exception in exceptions) { set.Add(exception); } EnsureEnumHasAvailableValues(enumValues, set); return SelectEnumValue(enumValues, set); } public Guid NextGuid() { return new Guid(GenerateGuidBytes(_guidBytes)); } public WGuid NextWGuid() { return new WGuid(GenerateGuidBytes(_guidBytes)); } private byte[] GenerateGuidBytes(byte[] guidBytes) { NextBytes(guidBytes); SetUuidV4Bits(guidBytes); return guidBytes; } public static void SetUuidV4Bits(byte[] bytes) { if (bytes == null || bytes.Length < 16) { throw new ArgumentException( "UUID buffer must contain at least 16 bytes.", nameof(bytes) ); } // Version bits live in byte 7 (second byte of the time_hi_and_version field). byte versionByte = bytes[7]; versionByte &= 0x0F; versionByte |= 0x40; bytes[7] = versionByte; // Variant bits live in byte 8 (clock_seq_hi_and_reserved). byte variantByte = bytes[8]; variantByte &= 0x3F; variantByte |= 0x80; bytes[8] = variantByte; } public float[,] NextNoiseMap( float[,] noiseMap, PerlinNoise noise = null, float scale = 2.5f, int octaves = 8, float persistence = 0.5f, float lacunarity = 2f, Vector2 baseOffset = default, float octaveOffsetRange = 100000f, bool normalize = true ) { if (noiseMap is null) { throw new ArgumentNullException(nameof(noiseMap)); } if (scale <= 0) { throw new ArgumentException(nameof(scale)); } if (octaves < 1) { throw new ArgumentException(nameof(octaves)); } if (persistence <= 0) { throw new ArgumentException(nameof(persistence)); } if (lacunarity <= 0) { throw new ArgumentException(nameof(lacunarity)); } if (octaveOffsetRange <= 0) { throw new ArgumentException(nameof(octaveOffsetRange)); } noise ??= PerlinNoise.Instance; int width = noiseMap.GetLength(0); int height = noiseMap.GetLength(1); using PooledArray octaveOffsetBuffer = SystemArrayPool.Get( octaves, out Vector2[] octaveOffsets ); for (int i = 0; i < octaves; i++) { float offsetX = NextFloat(-octaveOffsetRange, octaveOffsetRange); float offsetY = NextFloat(-octaveOffsetRange, octaveOffsetRange); octaveOffsets[i] = new Vector2(offsetX, offsetY); } float maxNoiseHeight = float.MinValue; float minNoiseHeight = float.MaxValue; float halfWidth = width / 2f; float halfHeight = height / 2f; for (int x = 0; x < width; ++x) { for (int y = 0; y < height; ++y) { float amplitude = 1; float frequency = 1; float noiseHeight = 0; for (int i = 0; i < octaves; i++) { float sampleX = (x - halfWidth) / scale * frequency + octaveOffsets[i].x + baseOffset.x; float sampleY = (y - halfHeight) / scale * frequency + octaveOffsets[i].y + baseOffset.y; float perlinValue = noise.Noise(sampleX, sampleY) * 2 - 1; noiseHeight += perlinValue * amplitude; amplitude *= persistence; frequency *= lacunarity; } if (noiseHeight > maxNoiseHeight) { maxNoiseHeight = noiseHeight; } else if (noiseHeight < minNoiseHeight) { minNoiseHeight = noiseHeight; } noiseMap[x, y] = noiseHeight; } } if (normalize) { for (int x = 0; x < width; ++x) { for (int y = 0; y < height; ++y) { // Returns a value between 0f and 1f based on noiseMap value // minNoiseHeight being 0f, and maxNoiseHeight being 1f noiseMap[x, y] = Mathf.InverseLerp( minNoiseHeight, maxNoiseHeight, noiseMap[x, y] ); } } } return noiseMap; } protected T RandomOf(IReadOnlyList values) { int count = values.Count; return count switch { 0 => default, 1 => values[0], 2 => NextBool() ? values[0] : values[1], _ => values[Next(count)], }; } public abstract IRandom Copy(); [MethodImpl(MethodImplOptions.AggressiveInlining)] protected internal static uint RotateLeft(uint value, int count) { return (value << count) | (value >> (32 - count)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] protected internal static uint Mix32(ref ulong state) { unchecked { state += 0x9E3779B97F4A7C15UL; ulong z = state; z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9UL; z = (z ^ (z >> 27)) * 0x94D049BB133111EBUL; z ^= z >> 31; return (uint)z; } } } }