// MIT License - Copyright (c) 2025 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Core.DataStructure.Adapters { using System; using System.Buffers.Binary; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Text.Json.Serialization; using Helper; using ProtoBuf; using UnityEngine; /// /// Immutable wrapper around that stores a normalized version-4 GUID using two longs for faster Unity serialization. /// /// /// The structure enforces version-4 GUIDs so that values generated through Unity serialization or manual assignment remain compatible across formats. /// /// /// /// [Serializable] [DataContract] [ProtoContract] public struct WGuid : IEquatable, IEquatable, IComparable, IComparable, IComparable, IFormattable { /// /// Sentinel instance representing the default . /// public static readonly WGuid EmptyGuid = default; /// /// Gets an empty value equivalent to . /// public static WGuid Empty => default; internal const string LowFieldName = nameof(_low); internal const string HighFieldName = nameof(_high); internal const string GuidPropertyName = nameof(Guid); [ProtoMember(1)] [SerializeField] private long _low; [ProtoMember(2)] [SerializeField] private long _high; [JsonInclude] [DataMember] private string Guid => ToString(); /// /// Generates a new random version-4 . /// /// A newly generated GUID wrapper. /// /// /// WGuid levelId = WGuid.NewGuid(); /// /// public static WGuid NewGuid() { return new WGuid(global::System.Guid.NewGuid()); } /// /// Initializes the wrapper from a instance. /// /// The source GUID. Must be version 4. /// Thrown when the provided GUID is not version 4. /// /// /// WGuid wrapped = new WGuid(Guid.NewGuid()); /// /// public WGuid(Guid guid) { _low = 0L; _high = 0L; SetFromGuid(guid); } /// /// Initializes the wrapper from a textual GUID representation. /// /// A string containing a GUID in any supported format. /// Thrown when is null. /// Thrown when the value is not a version-4 GUID. /// /// /// WGuid restored = new WGuid(\"2f3a9b4c-8d1f-4cba-8df7-2af00f5c6c1e\"); /// /// [JsonConstructor] public WGuid(string guid) : this(ParseGuidString(guid)) { } /// /// Initializes the wrapper from a 16-byte GUID array. /// /// The byte array that contains the GUID. /// Thrown when is null. /// Thrown when the array is not exactly 16 bytes. /// /// /// byte[] data = Guid.NewGuid().ToByteArray(); /// WGuid wrapped = new WGuid(data); /// /// public WGuid(ReadOnlySpan guidBytes) { if (guidBytes == null) { throw new ArgumentNullException(nameof(guidBytes)); } if (guidBytes.Length != 16) { throw new ArgumentOutOfRangeException(nameof(guidBytes)); } _low = 0L; _high = 0L; SetFromBytes(guidBytes); } /// /// Converts a into a . /// /// The wrapper to convert. /// The underlying value. /// /// /// Guid systemGuid = WGuid.NewGuid(); /// /// public static implicit operator Guid(WGuid guid) { return guid.ToGuid(); } /// /// Wraps a system inside a . /// /// The GUID to wrap. /// A that stores the provided GUID. /// /// /// WGuid wrapped = Guid.NewGuid(); /// /// public static implicit operator WGuid(Guid guid) { return new WGuid(guid); } internal static WGuid CreateUnchecked(long low, long high) { WGuid guid = default; guid._low = low; guid._high = high; return guid; } /// /// Determines equality between two wrappers by comparing their packed representations. /// /// The left-hand value. /// The right-hand value. /// true when both wrappers refer to the same GUID. /// /// /// bool same = WGuid.Empty == default; /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool operator ==(WGuid lhs, WGuid rhs) { return lhs.Equals(rhs); } /// /// Determines inequality between two wrappers. /// /// The left-hand value. /// The right-hand value. /// true when the GUIDs differ. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool operator !=(WGuid lhs, WGuid rhs) { return !lhs.Equals(rhs); } /// /// Parses a textual GUID into a , enforcing version 4 semantics. /// /// The GUID string to parse. /// A new wrapping the parsed value. /// Thrown when is null. /// Thrown when the string is not a version-4 GUID. /// /// /// WGuid parsed = WGuid.Parse(\"2f3a9b4c-8d1f-4cba-8df7-2af00f5c6c1e\"); /// /// public static WGuid Parse(string value) { Guid parsed = ParseGuidString(value); return new WGuid(parsed); } /// /// Attempts to parse a textual GUID, enforcing version 4 semantics. /// /// The GUID string to parse. /// When this method returns, contains the parsed GUID wrapper or . /// true when parsing succeeds. /// /// /// if (WGuid.TryParse(input, out WGuid guid)) { Use(guid); } /// /// public static bool TryParse(string value, out WGuid guid) { if ( global::System.Guid.TryParse(value, out Guid parsed) && IsVersionFour(parsed, out _) ) { guid = new WGuid(parsed); return true; } guid = EmptyGuid; return false; } /// /// Attempts to parse a span-based GUID representation, enforcing version 4 semantics. /// /// The characters representing the GUID. /// When successful, receives the parsed wrapper. /// true when parsing succeeded. public static bool TryParse(ReadOnlySpan value, out WGuid guid) { if ( global::System.Guid.TryParse(value, out Guid parsed) && IsVersionFour(parsed, out _) ) { guid = new WGuid(parsed); return true; } guid = EmptyGuid; return false; } /// /// Formats the GUID into the provided destination span. /// /// The target buffer that receives the characters. /// Outputs the number of characters written. /// Optional format specifier compatible with . /// true when the destination buffer was large enough to receive the formatting. /// /// buffer = stackalloc char[36]; /// guid.TryFormat(buffer, out int written); /// ]]> /// public bool TryFormat( Span destination, out int charsWritten, ReadOnlySpan format = default ) { Guid guid = ToGuid(); return guid.TryFormat(destination, out charsWritten, format); } /// /// Compares two wrappers for equality based on their packed longs. /// /// The other wrapper to compare against. /// true when both wrappers represent the same GUID. [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Equals(WGuid other) { return _low == other._low && _high == other._high; } /// /// Compares this wrapper for equality with a . /// /// The GUID to compare against. /// true when both values represent the same GUID. public bool Equals(Guid other) { WGuid converted = new(other); return Equals(converted); } /// public override bool Equals(object obj) { return obj switch { WGuid otherWGuid => Equals(otherWGuid), Guid otherGuid => Equals(otherGuid), _ => false, }; } /// /// Compares this wrapper with another for ordering. /// /// The other wrapper. /// A signed comparison value compatible with . public int CompareTo(WGuid other) { Guid self = ToGuid(); Guid otherGuid = other.ToGuid(); return self.CompareTo(otherGuid); } /// /// Compares this wrapper with a for ordering. /// /// The GUID to compare with. /// A signed comparison value. public int CompareTo(Guid other) { Guid self = ToGuid(); return self.CompareTo(other); } /// public int CompareTo(object obj) { return obj switch { WGuid otherWGuid => CompareTo(otherWGuid), Guid otherGuid => CompareTo(otherGuid), _ => -1, }; } /// /// Returns a hash that combines the packed 128-bit value so instances can be used inside hash sets and dictionaries. /// /// A stable hash derived from the underlying GUID. /// /// pending = new HashSet(); /// WGuid identifier = WGuid.NewGuid(); /// pending.Add(identifier); /// int hash = identifier.GetHashCode(); /// ]]> /// public override int GetHashCode() { return Objects.HashCode(_low, _high); } /// /// Returns the standard string representation of the underlying GUID. /// public override string ToString() { return ToGuid().ToString(); } /// /// Formats the GUID using the specified format and format provider. /// /// The format string. /// Format provider for culture-specific formatting. /// The formatted string. public string ToString(string format, IFormatProvider formatProvider) { Guid self = ToGuid(); return self.ToString(format, formatProvider); } /// /// Creates a new byte array containing the GUID bytes. /// /// A 16-byte array representing the GUID. /// /// /// byte[] bytes = guid.ToByteArray(); /// File.WriteAllBytes(path, bytes); /// /// public byte[] ToByteArray() { byte[] bytes = new byte[16]; Span span = bytes.AsSpan(); WriteBytes(span); return bytes; } /// /// Attempts to write the GUID bytes into the provided destination span. /// /// The buffer receiving the GUID bytes. /// true when is at least 16 bytes. public bool TryWriteBytes(Span destination) { if (destination.Length < 16) { return false; } WriteBytes(destination); return true; } /// /// Gets a value indicating whether the wrapper stores the empty GUID. /// public bool IsEmpty => _low == 0L && _high == 0L; /// /// Gets the GUID version encoded in the wrapper. /// public int Version { get { ulong low = unchecked((ulong)_low); return ExtractVersion(low); } } /// /// Gets a value indicating whether the GUID represents a random version-4 value. /// public bool IsVersion4 => Version == 4; /// /// Gets a value indicating whether the stored value is either empty or a valid version-4 GUID. /// public bool IsValid => HasVersionFourLayout(_low, _high); /// /// Converts the wrapper back to a instance. /// /// The underlying GUID. /// /// /// Guid systemGuid = guid.ToGuid(); /// /// public Guid ToGuid() { Span buffer = stackalloc byte[16]; WriteBytes(buffer); return new Guid(buffer); } private void SetFromGuid(Guid guid) { Span buffer = stackalloc byte[16]; bool success = guid.TryWriteBytes(buffer); if (!success) { throw new InvalidOperationException("Failed to convert Guid to bytes."); } SetFromBytes(buffer); } private void SetFromBytes(ReadOnlySpan bytes) { if (bytes.Length != 16) { throw new ArgumentOutOfRangeException(nameof(bytes)); } ulong low = BinaryPrimitives.ReadUInt64LittleEndian(bytes.Slice(0, 8)); ulong high = BinaryPrimitives.ReadUInt64LittleEndian(bytes.Slice(8, 8)); EnsureVersionFourLayout(low, high); _low = unchecked((long)low); _high = unchecked((long)high); } private void WriteBytes(Span destination) { ulong low = unchecked((ulong)_low); ulong high = unchecked((ulong)_high); BinaryPrimitives.WriteUInt64LittleEndian(destination.Slice(0, 8), low); BinaryPrimitives.WriteUInt64LittleEndian(destination.Slice(8, 8), high); } private static Guid ParseGuidString(string value) { if (value == null) { throw new ArgumentNullException(nameof(value)); } Guid parsed = global::System.Guid.Parse(value); if (!IsVersionFour(parsed, out int version)) { throw CreateNonVersionFourException(version); } return parsed; } private static bool IsVersionFour(Guid guid, out int version) { Span buffer = stackalloc byte[16]; version = -1; bool success = guid.TryWriteBytes(buffer); if (!success) { return false; } ulong low = BinaryPrimitives.ReadUInt64LittleEndian(buffer.Slice(0, 8)); version = ExtractVersion(low); return version == 4; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int ExtractVersion(ulong low) { ushort segment = (ushort)((low >> 48) & 0xFFFF); return (segment >> 12) & 0x0F; } internal static bool HasVersionFourLayout(long low, long high) { if (low == 0L && high == 0L) { return true; } return HasVersionFourBits(unchecked((ulong)low)); } private static void EnsureVersionFourLayout(ulong low, ulong high) { if (low == 0UL && high == 0UL) { return; } int version = ExtractVersion(low); if (version != 4) { throw CreateNonVersionFourException(version); } } private static bool HasVersionFourBits(ulong low) { return ExtractVersion(low) == 4; } private static FormatException CreateNonVersionFourException(int? detectedVersion = null) { if (detectedVersion is >= 0) { return new FormatException( $"{nameof(WGuid)} requires a version 4 {nameof(Guid)}, but found version {detectedVersion.Value}." ); } return new FormatException($"{nameof(WGuid)} requires a version 4 {nameof(Guid)}."); } } }