Improve MongoIds (#537)

* Mongoid improvements pt1

* Format Style Fixes

* Fix incorrect string

* Fix != comparison

---------

Co-authored-by: sp-tarkov-bot <singleplayertarkov@gmail.com>
This commit is contained in:
Lacyway
2025-08-09 12:50:49 +02:00
committed by GitHub
parent f3134dec59
commit d468848b0c
25 changed files with 183 additions and 104 deletions
@@ -24,13 +24,13 @@ public class LauncherCallbacks(
public ValueTask<string> Login(string url, LoginRequestData info, MongoId sessionID)
{
var output = launcherController.Login(info);
return new ValueTask<string>(output.IsEmpty() ? "FAILED" : output.ToString());
return new ValueTask<string>(output.IsEmpty ? "FAILED" : output.ToString());
}
public async ValueTask<string> Register(string url, RegisterData info, MongoId sessionID)
{
var output = await launcherController.Register(info);
return output.IsEmpty() ? string.Empty : output.ToString();
return output.IsEmpty ? string.Empty : output.ToString();
}
public ValueTask<string> Get(string url, LoginRequestData info, MongoId sessionID)
@@ -54,7 +54,7 @@ public class LauncherCallbacks(
public ValueTask<string> Wipe(string url, RegisterData info, MongoId sessionID)
{
var output = launcherController.Wipe(info);
return new ValueTask<string>(output.IsEmpty() ? "FAILED" : "OK");
return new ValueTask<string>(output.IsEmpty ? "FAILED" : "OK");
}
public ValueTask<string> GetServerVersion()
@@ -52,7 +52,7 @@ public class GameController(
{
profileActivityService.AddActiveProfile(sessionId, startTimeStampMs);
if (sessionId.IsEmpty())
if (sessionId.IsEmpty)
{
logger.Error($"{nameof(sessionId)} is empty on GameController.GameStart");
return;
@@ -192,7 +192,7 @@ public class HideoutController(
}
// Upgrade includes a container improvement/addition
if (hideoutStage.Container.HasValue && !hideoutStage.Container.Value.IsEmpty())
if (hideoutStage.Container.HasValue && !hideoutStage.Container.Value.IsEmpty)
{
AddContainerImprovementToProfile(output, sessionID, pmcData, profileHideoutArea, hideoutData, hideoutStage);
}
@@ -144,7 +144,7 @@ public class LauncherController(
{
var sessionID = Login(info);
if (!sessionID.IsEmpty())
if (!sessionID.IsEmpty)
{
saveServer.GetProfile(sessionID).ProfileInfo!.Username = info.Change;
}
@@ -182,7 +182,7 @@ public class LauncherController(
var sessionId = Login(info);
if (!sessionId.IsEmpty())
if (!sessionId.IsEmpty)
{
var profileInfo = saveServer.GetProfile(sessionId).ProfileInfo;
profileInfo!.Edition = info.Edition;
@@ -62,7 +62,7 @@ public class LauncherV2Controller(
{
var sessionId = GetSessionId(info);
return !sessionId.IsEmpty();
return !sessionId.IsEmpty;
}
/// <summary>
@@ -93,7 +93,7 @@ public class LauncherV2Controller(
{
var sessionId = GetSessionId(info);
if (sessionId.IsEmpty())
if (sessionId.IsEmpty)
{
return false;
}
@@ -117,7 +117,7 @@ public class LauncherV2Controller(
{
var sessionId = GetSessionId(info);
return !sessionId.IsEmpty() && saveServer.RemoveProfile(sessionId);
return !sessionId.IsEmpty && saveServer.RemoveProfile(sessionId);
}
/// <summary>
@@ -322,7 +322,7 @@ public class RagfairController(
return ragfairOfferHelper.GetOffersForBuild(searchRequest, itemsToAdd, traderAssorts, pmcProfile);
}
if (searchRequest.NeededSearchId != null && !searchRequest.NeededSearchId.Value.IsEmpty())
if (searchRequest.NeededSearchId != null && !searchRequest.NeededSearchId.Value.IsEmpty)
{
return ragfairOfferHelper.GetOffersThatRequireItem(searchRequest, pmcProfile);
}
@@ -931,7 +931,7 @@ public class RagfairController(
{
return requirements.Sum(requirement =>
{
if (requirement.Template.IsEmpty() || !requirement.Count.HasValue || requirement.Count == 0)
if (requirement.Template.IsEmpty || !requirement.Count.HasValue || requirement.Count == 0)
{
return 0;
}
@@ -378,7 +378,7 @@ public class RepeatableQuestController(
.ToList();
var traderId = randomUtil.DrawRandomFromList(traders).FirstOrDefault();
if (traderId.IsEmpty())
if (traderId.IsEmpty)
{
logger.Error(serverLocalisationService.GetText("repeatable-unable_to_find_trader_in_pool"));
@@ -254,7 +254,7 @@ public static class ProfileExtensions
/// <param name="output">OPTIONAL - ItemEventRouterResponse</param>
public static void RemoveItem(this PmcData profile, MongoId itemId, MongoId sessionId, ItemEventRouterResponse? output = null)
{
if (itemId.IsEmpty())
if (itemId.IsEmpty)
{
return;
}
@@ -204,7 +204,7 @@ public class BotEquipmentModGenerator(
{
modTpl = exhaustableModPool.GetRandomValue();
if (
!modTpl.Value.IsEmpty()
!modTpl.Value.IsEmpty
&& !botGeneratorHelper
.IsItemIncompatibleWithCurrentItems(equipment, modTpl.Value, modSlotName)
.Incompatible.GetValueOrDefault(false)
@@ -446,7 +446,7 @@ public class BotWeaponGenerator(
}
// Has an UBGL
if (generatedWeaponResult.ChosenUbglAmmoTemplate is not null && !generatedWeaponResult.ChosenUbglAmmoTemplate.Value.IsEmpty())
if (generatedWeaponResult.ChosenUbglAmmoTemplate is not null && !generatedWeaponResult.ChosenUbglAmmoTemplate.Value.IsEmpty)
{
AddUbglGrenadesToBotInventory(weaponAndMods, generatedWeaponResult, inventory);
}
@@ -255,7 +255,7 @@ public class FenceBaseAssortGenerator(
{
var modItemDbDetails = itemHelper.GetItem(requiredSlot.Props.Filters.First().Plate.Value).Value;
var plateTpl = requiredSlot.Props.Filters.First().Plate; // `Plate` property appears to be the 'default' item for slot
if (plateTpl is null || plateTpl.Value.IsEmpty())
if (plateTpl is null || plateTpl.Value.IsEmpty)
// Some bsg plate properties are empty, skip mod
{
continue;
@@ -119,7 +119,7 @@ public class RagfairAssortGenerator(
/// <returns> Hydrated Item object </returns>
protected Item CreateRagfairAssortRootItem(MongoId tplId, MongoId? id = null)
{
if (id == null || id.Value.IsEmpty())
if (id == null || id.Value.IsEmpty)
{
id = new MongoId();
}
@@ -436,7 +436,7 @@ public class InventoryHelper(
/// <param name="output">OPTIONAL - ItemEventRouterResponse</param>
public void RemoveItem(PmcData profile, MongoId itemId, MongoId sessionId, ItemEventRouterResponse? output = null)
{
if (itemId.IsEmpty())
if (itemId.IsEmpty)
{
logger.Warning(serverLocalisationService.GetText("inventory-unable_to_remove_item_no_id_given"));
@@ -562,7 +562,7 @@ public class InventoryHelper(
ItemEventRouterResponse output
)
{
if (itemId.IsEmpty())
if (itemId.IsEmpty)
{
return output;
}
@@ -1670,7 +1670,7 @@ public class ItemHelper(
}
var chosenTpl = GetCompatibleTplFromArray(itemPool, incompatibleModTpls);
if (chosenTpl.IsEmpty())
if (chosenTpl.IsEmpty)
{
if (logger.IsLogEnabled(LogLevel.Debug))
{
@@ -92,7 +92,7 @@ public class PresetHelper(DatabaseService databaseService, ItemHelper itemHelper
/// <returns>True = preset exists for this id</returns>
public bool IsPreset(MongoId id)
{
if (id.IsEmpty())
if (id.IsEmpty)
{
return false;
}
@@ -88,7 +88,7 @@ public class RagfairHelper(
}
// Case: category
if (request.HandbookId.HasValue && !request.HandbookId.Value.IsEmpty())
if (request.HandbookId.HasValue && !request.HandbookId.Value.IsEmpty)
{
var handbook = GetCategoryList(request.HandbookId.Value);
result = (result?.Count > 0 ? result.IntersectWith(handbook) : handbook).ToList();
@@ -365,7 +365,7 @@ public class RagfairOfferHelper(
// Performing a required search and offer doesn't have requirement for item
if (
!searchRequest.NeededSearchId.HasValue
&& !searchRequest.NeededSearchId.Value.IsEmpty()
&& !searchRequest.NeededSearchId.Value.IsEmpty
&& !offer.Requirements.Any(requirement => requirement.TemplateId == searchRequest.NeededSearchId)
)
{
@@ -1,71 +1,140 @@
using SPTarkov.Server.Core.Extensions;
using System.Buffers.Binary;
using System.Security.Cryptography;
using SPTarkov.Server.Core.Extensions;
namespace SPTarkov.Server.Core.Models.Common;
/// <summary>
/// Represents a 12-byte MongoDB-style ObjectId, consisting of:
/// <list type="bullet">
/// <item><description>4-byte timestamp (seconds since Unix epoch, big-endian)</description></item>
/// <item><description>3-byte machine identifier</description></item>
/// <item><description>2-byte process identifier (big-endian)</description></item>
/// <item><description>3-byte incrementing counter (big-endian)</description></item>
/// </list>
/// </summary>
/// <remarks>
/// <para>
/// This struct stores the ObjectId in two packed fields for efficient memory usage
/// and comparison:
/// <list type="bullet">
/// <item><see cref="_timestampAndMachine"/><description>: First 8 bytes (timestamp + machine ID)</description></item>
/// <item><see cref="_pidAndIncrement"/><description>: Last 4 bytes (process ID + counter)</description></item>
/// </list>
/// </para>
/// <para>
/// The struct is immutable and implements <see cref="IEquatable{MongoId}"/> for fast comparisons.
/// </para>
/// </remarks>
public readonly struct MongoId : IEquatable<MongoId>, IComparable<MongoId>
{
private readonly string? _stringId;
/// <summary>
/// The first 8 bytes: 4-byte timestamp + 3-byte machine ID + 1 byte of PID.
/// </summary>
private readonly long _timestampAndMachine;
public MongoId(string? id)
/// <summary>
/// The last 4 bytes: remaining 1 byte of PID + 3-byte counter.
/// </summary>
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
{
// Handle null strings, various id's are null either by BSG or by our own doing with LINQ
if (string.IsNullOrEmpty(id))
{
_stringId = null;
return;
}
if (id.Length != 24)
{
// TODO: Items.json root item has an empty parentId property
Console.WriteLine($"Critical MongoId error: Incorrect length. id: {id}");
}
if (!IsValidMongoId(id))
{
Console.WriteLine($"Critical MongoId error: Incorrect format. Must be a hexadecimal [a-f0-9] of 24 characters. id: {id}");
}
_stringId = string.Intern(id);
}
public MongoId()
{
_stringId = Generate();
get { return _timestampAndMachine == 0 && _pidAndIncrement == 0; }
}
/// <summary>
/// Create a 24 character MongoId
/// Initializes a new <see cref="MongoId"/> with a generated value
/// based on the current time, machine ID, process ID, and counter.
/// </summary>
/// <returns>24 character objectId</returns>
private static string Generate()
public MongoId()
{
Span<byte> objectId = stackalloc byte[12];
// 4 bytes: current timestamp (big endian)
var timestamp = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
objectId[0] = (byte)(timestamp >> 24);
objectId[1] = (byte)(timestamp >> 16);
objectId[2] = (byte)(timestamp >> 8);
objectId[3] = (byte)timestamp;
Span<byte> bytes = stackalloc byte[12];
// 5 bytes: random machine/process identifier
Random.Shared.NextBytes(objectId.Slice(4, 5));
// timestamp (4 bytes, big-endian)
BinaryPrimitives.WriteInt32BigEndian(bytes, timestamp);
// 3 bytes: random counter fallback (no static state)
var counter = Random.Shared.Next(0, 0xFFFFFF);
objectId[9] = (byte)(counter >> 16);
objectId[10] = (byte)(counter >> 8);
objectId[11] = (byte)counter;
// machine ID (3 bytes)
bytes[4] = (byte)(_machine >> 16);
bytes[5] = (byte)(_machine >> 8);
bytes[6] = (byte)_machine;
// Convert to lowercase hex string (24 chars)
return Convert.ToHexStringLower(objectId);
// 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<byte> bytes = stackalloc byte[12];
Span<char> 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;
}
/// <summary>
/// Returns the MongoId as a 24-character lowercase hexadecimal string.
/// </summary>
public override string ToString()
{
return _stringId ?? string.Empty;
if (_timestampAndMachine == 0 && _pidAndIncrement == 0)
{
return string.Empty;
}
Span<byte> bytes = stackalloc byte[12];
BitConverter.TryWriteBytes(bytes, _timestampAndMachine);
BitConverter.TryWriteBytes(bytes[8..], _pidAndIncrement);
return Convert.ToHexString(bytes).ToLowerInvariant();
}
public bool Equals(MongoId? other)
@@ -75,17 +144,39 @@ public readonly struct MongoId : IEquatable<MongoId>, IComparable<MongoId>
return false;
}
return other.ToString().Equals(ToString(), StringComparison.InvariantCultureIgnoreCase);
return _timestampAndMachine == other.Value._timestampAndMachine && _pidAndIncrement == other.Value._pidAndIncrement;
}
/// <inheritdoc/>
public bool Equals(MongoId other)
{
return _timestampAndMachine == other._timestampAndMachine && _pidAndIncrement == other._pidAndIncrement;
}
public bool Equals(string? other)
{
if (other is null)
if (other == null || other.Length != 24)
{
return _stringId == null;
return false;
}
return other.Equals(ToString(), StringComparison.InvariantCultureIgnoreCase);
Span<byte> 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)
@@ -103,16 +194,13 @@ public readonly struct MongoId : IEquatable<MongoId>, IComparable<MongoId>
return new MongoId(mongoId);
}
public bool Equals(MongoId other)
{
return string.Equals(_stringId, other._stringId, StringComparison.OrdinalIgnoreCase);
}
public int CompareTo(MongoId other)
{
return string.CompareOrdinal(_stringId, other._stringId);
var compare = _timestampAndMachine.CompareTo(other._timestampAndMachine);
return compare != 0 ? compare : _pidAndIncrement.CompareTo(other._pidAndIncrement);
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{
return obj is MongoId other && Equals(other);
@@ -125,7 +213,7 @@ public readonly struct MongoId : IEquatable<MongoId>, IComparable<MongoId>
public static bool operator !=(MongoId left, MongoId? right)
{
return left.Equals(right);
return !left.Equals(right);
}
public static bool operator ==(MongoId left, MongoId? right)
@@ -138,19 +226,10 @@ public readonly struct MongoId : IEquatable<MongoId>, IComparable<MongoId>
return !left.Equals(right);
}
/// <inheritdoc/>
public override int GetHashCode()
{
return (_stringId ?? string.Empty).GetHashCode();
}
public bool IsEmpty()
{
if (string.IsNullOrEmpty(_stringId) || _stringId == "000000000000000000000000")
{
return true;
}
return false;
return HashCode.Combine(_timestampAndMachine, _pidAndIncrement);
}
public static MongoId Empty()
@@ -106,7 +106,7 @@ public class SaveServer(
/// <exception cref="Exception"> Thrown when sessionId is null / empty or no profiles with that ID are found </exception>
public SptProfile GetProfile(MongoId sessionId)
{
if (sessionId.IsEmpty())
if (sessionId.IsEmpty)
{
throw new Exception("session id provided was empty, did you restart the server while the game was running?");
}
@@ -219,7 +219,7 @@ public class AirdropService(
var itemTypeBlacklist = _itemFilterService.GetItemRewardBaseTypeBlacklist();
var itemsMatchingTypeBlacklist = databaseService
.GetItems()
.Where(kvp => !kvp.Value.Parent.IsEmpty())
.Where(kvp => !kvp.Value.Parent.IsEmpty)
.Where(kvp => _itemHelper.IsOfBaseclasses(kvp.Value.Parent, itemTypeBlacklist))
.Select(kvp => kvp.Key)
.ToHashSet();
@@ -1158,7 +1158,7 @@ public class FenceService(
var durabilityValues = GetRandomisedArmorDurabilityValues(modItemDbDetails, traderConfig.Fence.ArmorMaxDurabilityPercentMinMax);
var plateTpl = requiredSlot.Props.Filters.First().Plate ?? string.Empty; // "Plate" property appears to be the 'default' item for slot
if (plateTpl.IsEmpty())
if (plateTpl.IsEmpty)
// Some bsg plate properties are empty, skip mod
{
continue;
@@ -1206,7 +1206,7 @@ public class FenceService(
foreach (var plateSlot in plateSlots)
{
var plateTpl = plateSlot.Props.Filters.First().Plate;
if (plateTpl == null || plateTpl.Value.IsEmpty())
if (plateTpl == null || plateTpl.Value.IsEmpty)
// Bsg data lacks a default plate, skip randomising for this mod
{
continue;
@@ -65,7 +65,7 @@ public class ItemBaseClassService(
_itemBaseClassesCache[itemIdToUpdate].Add(item.Parent);
databaseService.GetItems().TryGetValue(item.Parent, out var parent);
if (parent is not null && !parent.Parent.IsEmpty())
if (parent is not null && !parent.Parent.IsEmpty)
{
AddBaseItems(itemIdToUpdate, parent);
}
@@ -84,7 +84,7 @@ public class ItemBaseClassService(
HydrateItemBaseClassCache();
}
if (itemTpl.IsEmpty())
if (itemTpl.IsEmpty)
{
logger.Warning("Unable to check itemTpl base class as value passed is null");
@@ -136,7 +136,7 @@ public class ItemBaseClassService(
HydrateItemBaseClassCache();
}
if (itemTpl.IsEmpty())
if (itemTpl.IsEmpty)
{
logger.Warning("Unable to check itemTpl base class as value passed is null");
@@ -56,7 +56,7 @@ public class NotificationService
/// <param name="sessionID">Session/player id</param>
public List<WsNotificationEvent> Get(MongoId sessionID)
{
if (sessionID.IsEmpty())
if (sessionID.IsEmpty)
{
throw new Exception("sessionID missing");
}
@@ -581,7 +581,7 @@ public class ProfileFixerService(
protected Bonus? GetBonusFromProfile(IEnumerable<Bonus>? profileBonuses, Bonus bonus)
{
// match by id first, used by "TextBonus" bonuses
if (!bonus.Id.IsEmpty())
if (!bonus.Id.IsEmpty)
{
return profileBonuses?.FirstOrDefault(x => x.Id == bonus.Id);
}
@@ -143,7 +143,7 @@ public class RagfairOfferHolder(
var itemTpl = offer.Items?.FirstOrDefault()?.Template ?? new MongoId();
if (
!itemTpl.IsEmpty() // Has tpl
!itemTpl.IsEmpty // Has tpl
&& offer.IsFakePlayerOffer()
&& _fakePlayerOffers.TryGetValue(itemTpl, out var offers)
&& offers?.Count >= _ragfairServerHelper.GetOfferCountByBaseType(_itemHelper.GetItem(itemTpl).Value.Parent)