97 lines
3.1 KiB
C#
97 lines
3.1 KiB
C#
using System.Collections.Concurrent;
|
|
using NeonSprawl.Server.Game.Rewards;
|
|
|
|
namespace NeonSprawl.Server.Game.Factions;
|
|
|
|
/// <summary>Thread-safe in-memory append-only reputation delta audit log (NEO-135).</summary>
|
|
public sealed class InMemoryReputationDeltaStore : IReputationDeltaStore
|
|
{
|
|
private readonly ConcurrentDictionary<string, ReputationDeltaRow> rowsById = new(StringComparer.Ordinal);
|
|
|
|
private readonly ConcurrentDictionary<string, object> idLocks = new(StringComparer.Ordinal);
|
|
|
|
/// <inheritdoc />
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IReadOnlyList<ReputationDeltaRow> GetDeltasForPlayer(string playerId, int? limit = null)
|
|
{
|
|
var player = FactionStandingIds.NormalizePlayerId(playerId);
|
|
if (player.Length == 0)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
IEnumerable<ReputationDeltaRow> 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];
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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;
|
|
}
|