using System.Collections.Concurrent; using NeonSprawl.Server.Game.Rewards; namespace NeonSprawl.Server.Game.Factions; /// Thread-safe in-memory append-only reputation delta audit log (NEO-135). public sealed class InMemoryReputationDeltaStore : IReputationDeltaStore { private readonly ConcurrentDictionary rowsById = new(StringComparer.Ordinal); private readonly ConcurrentDictionary idLocks = new(StringComparer.Ordinal); /// public bool TryAppend(ReputationDeltaRow row) { if (!IsValidRow(row)) { return false; } var normalized = row with { Id = row.Id.Trim(), PlayerId = FactionStandingIds.NormalizePlayerId(row.PlayerId), FactionId = FactionStandingIds.NormalizeFactionId(row.FactionId), SourceKind = row.SourceKind.Trim(), SourceId = row.SourceId.Trim(), }; lock (idLocks.GetOrAdd(normalized.Id, _ => new object())) { if (rowsById.ContainsKey(normalized.Id)) { return false; } rowsById[normalized.Id] = normalized; return true; } } /// public IReadOnlyList GetDeltasForPlayer(string playerId, int? limit = null) { var player = FactionStandingIds.NormalizePlayerId(playerId); if (player.Length == 0) { return []; } IEnumerable query = rowsById.Values .Where(r => string.Equals(r.PlayerId, player, StringComparison.Ordinal)) .OrderBy(static r => r.RecordedAt) .ThenBy(static r => r.Id, StringComparer.Ordinal); if (limit is > 0) { query = query.Take(limit.Value); } return [..query]; } /// public bool TryClearQuestCompletionAudit(string playerId, string questId) { var player = FactionStandingIds.NormalizePlayerId(playerId); var sourceId = RewardDeliveryIds.NormalizeQuestId(questId); if (player.Length == 0 || sourceId.Length == 0) { return false; } string[] idsToRemove = [.. rowsById.Values .Where(row => string.Equals(row.PlayerId, player, StringComparison.Ordinal) && string.Equals(row.SourceKind, ReputationDeltaSourceKinds.QuestCompletion, StringComparison.Ordinal) && string.Equals(row.SourceId, sourceId, StringComparison.Ordinal)) .Select(static row => row.Id)]; foreach (var id in idsToRemove) { rowsById.TryRemove(id, out _); idLocks.TryRemove(id, out _); } return true; } private static bool IsValidRow(ReputationDeltaRow row) => row.Id.Trim().Length > 0 && FactionStandingIds.NormalizePlayerId(row.PlayerId).Length > 0 && FactionStandingIds.NormalizeFactionId(row.FactionId).Length > 0 && row.SourceKind.Trim().Length > 0 && row.SourceId.Trim().Length > 0; }