// 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)}.");
}
}
}