using System.Collections.Concurrent; namespace NeonSprawl.Server.Game.Gathering; /// Thread-safe in-memory node instance capacity; empty at startup — lazy rows only (NEO-61). public sealed class InMemoryResourceNodeInstanceStore : IResourceNodeInstanceStore { private readonly ConcurrentDictionary remainingById = new(StringComparer.Ordinal); private readonly ConcurrentDictionary idLocks = new(StringComparer.Ordinal); /// public bool TryGetRemainingGathers(string interactableId, out ResourceNodeInstanceSnapshot snapshot) { var key = NormalizeInteractableId(interactableId); if (key.Length == 0) { snapshot = default; return false; } lock (idLocks.GetOrAdd(key, _ => new object())) { if (!remainingById.TryGetValue(key, out var remaining)) { snapshot = default; return false; } snapshot = new ResourceNodeInstanceSnapshot(key, remaining); return true; } } /// public bool TryEnsureInitialized(string interactableId, int initialRemainingGathers) { var key = NormalizeInteractableId(interactableId); if (key.Length == 0 || initialRemainingGathers < 0) { return false; } lock (idLocks.GetOrAdd(key, _ => new object())) { if (remainingById.ContainsKey(key)) { return true; } remainingById[key] = initialRemainingGathers; return true; } } /// public bool TryDecrementRemainingGathers( string interactableId, out int previousRemaining, out int remainingGathers) { previousRemaining = 0; remainingGathers = 0; var key = NormalizeInteractableId(interactableId); if (key.Length == 0) { return false; } lock (idLocks.GetOrAdd(key, _ => new object())) { if (!remainingById.TryGetValue(key, out var current) || current <= 0) { if (current == 0) { remainingGathers = 0; } return false; } previousRemaining = current; remainingGathers = current - 1; remainingById[key] = remainingGathers; return true; } } internal static string NormalizeInteractableId(string? interactableId) { var trimmed = interactableId?.Trim(); if (string.IsNullOrEmpty(trimmed)) { return string.Empty; } return trimmed.ToLowerInvariant(); } }