using System.Buffers.Binary; using System.Security.Cryptography; using SPTarkov.Server.Core.Extensions; namespace SPTarkov.Server.Core.Models.Common; /// /// Represents a 12-byte MongoDB-style ObjectId, consisting of: /// /// 4-byte timestamp (seconds since Unix epoch, big-endian) /// 3-byte machine identifier /// 2-byte process identifier (big-endian) /// 3-byte incrementing counter (big-endian) /// /// /// /// /// This struct stores the ObjectId in two packed fields for efficient memory usage /// and comparison: /// /// : First 8 bytes (timestamp + machine ID) /// : Last 4 bytes (process ID + counter) /// /// /// /// The struct is immutable and implements for fast comparisons. /// /// public readonly struct MongoId : IEquatable, IComparable { /// /// The first 8 bytes: 4-byte timestamp + 3-byte machine ID + 1 byte of PID. /// private readonly long _timestampAndMachine; /// /// The last 4 bytes: remaining 1 byte of PID + 3-byte counter. /// private readonly int _pidAndIncrement; private static readonly int _machine = BitConverter.ToInt32(RandomNumberGenerator.GetBytes(4), 0) & 0xFFFFFF; private static readonly short _pid = (short)Environment.ProcessId; private static int _increment = RandomNumberGenerator.GetInt32(0, 0xFFFFFF); public bool IsEmpty { get { return _timestampAndMachine == 0 && _pidAndIncrement == 0; } } /// /// Initializes a new with a generated value /// based on the current time, machine ID, process ID, and counter. /// public MongoId() { var timestamp = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(); Span bytes = stackalloc byte[12]; // timestamp (4 bytes, big-endian) BinaryPrimitives.WriteInt32BigEndian(bytes, timestamp); // machine ID (3 bytes) bytes[4] = (byte)(_machine >> 16); bytes[5] = (byte)(_machine >> 8); bytes[6] = (byte)_machine; // PID (2 bytes) BinaryPrimitives.WriteInt16BigEndian(bytes[7..9], _pid); // increment (3 bytes, big-endian) var inc = Interlocked.Increment(ref _increment) & 0xFFFFFF; bytes[9] = (byte)(inc >> 16); bytes[10] = (byte)(inc >> 8); bytes[11] = (byte)inc; // pack into fields (avoids array allocations later) _timestampAndMachine = BitConverter.ToInt64(bytes); _pidAndIncrement = BitConverter.ToInt32(bytes[8..]); } public MongoId(string? hex) { if (string.IsNullOrEmpty(hex) || hex == "000000000000000000000000") { _timestampAndMachine = 0; _pidAndIncrement = 0; return; } if (hex.Length != 24) { throw new ArgumentException("ObjectId must be a 24-character hex string.", nameof(hex)); } Span bytes = stackalloc byte[12]; Span chars = stackalloc char[24]; hex.AsSpan().CopyTo(chars); for (var i = 0; i < 12; i++) { var hi = HexCharToValue(hex[2 * i]); var lo = HexCharToValue(hex[2 * i + 1]); if (hi == -1 || lo == -1) { throw new FormatException("ObjectId contains invalid hex characters."); } bytes[i] = (byte)((hi << 4) | lo); } _timestampAndMachine = BitConverter.ToInt64(bytes); _pidAndIncrement = BitConverter.ToInt32(bytes[8..]); } private static int HexCharToValue(char c) { return c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : c >= 'A' && c <= 'F' ? c - 'A' + 10 : -1; } /// /// Returns the MongoId as a 24-character lowercase hexadecimal string. /// public override string ToString() { if (_timestampAndMachine == 0 && _pidAndIncrement == 0) { return string.Empty; } Span bytes = stackalloc byte[12]; BitConverter.TryWriteBytes(bytes, _timestampAndMachine); BitConverter.TryWriteBytes(bytes[8..], _pidAndIncrement); return Convert.ToHexString(bytes).ToLowerInvariant(); } public bool Equals(MongoId? other) { if (other is null) { return false; } return _timestampAndMachine == other.Value._timestampAndMachine && _pidAndIncrement == other.Value._pidAndIncrement; } /// public bool Equals(MongoId other) { return _timestampAndMachine == other._timestampAndMachine && _pidAndIncrement == other._pidAndIncrement; } public bool Equals(string? other) { if (other == null || other.Length != 24) { return false; } Span bytes = stackalloc byte[12]; for (var i = 0; i < 12; i++) { var hi = HexCharToValue(other[2 * i]); var lo = HexCharToValue(other[2 * i + 1]); if (hi == -1 || lo == -1) { return false; } bytes[i] = (byte)((hi << 4) | lo); } var a = BitConverter.ToInt64(bytes); var b = BitConverter.ToInt32(bytes[8..]); return _timestampAndMachine == a && _pidAndIncrement == b; } public static bool IsValidMongoId(string stringToCheck) { return stringToCheck.IsValidMongoId(); } public static implicit operator string(MongoId mongoId) { return mongoId.ToString(); } public static implicit operator MongoId(string mongoId) { return new MongoId(mongoId); } public int CompareTo(MongoId other) { var compare = _timestampAndMachine.CompareTo(other._timestampAndMachine); return compare != 0 ? compare : _pidAndIncrement.CompareTo(other._pidAndIncrement); } /// public override bool Equals(object? obj) { return obj is MongoId other && Equals(other); } public static bool operator ==(MongoId left, MongoId right) { return left.Equals(right); } public static bool operator !=(MongoId left, MongoId? right) { return !left.Equals(right); } public static bool operator ==(MongoId left, MongoId? right) { return left.Equals(right); } public static bool operator !=(MongoId left, MongoId right) { return !left.Equals(right); } /// public override int GetHashCode() { return HashCode.Combine(_timestampAndMachine, _pidAndIncrement); } public static MongoId Empty() { return new MongoId("000000000000000000000000"); } }