using System.Collections.Concurrent; 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.ToArray(); } 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; }