neon-sprawl/server/NeonSprawl.Server/Game/Factions/PostgresReputationDeltaStor...

130 lines
4.9 KiB
C#

namespace NeonSprawl.Server.Game.Factions;
using NeonSprawl.Server.Game.Rewards;
/// <summary>PostgreSQL-backed append-only reputation delta audit log (NEO-135).</summary>
public sealed class PostgresReputationDeltaStore(Npgsql.NpgsqlDataSource dataSource) : IReputationDeltaStore
{
/// <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(),
};
PostgresReputationDeltaBootstrap.EnsureSchema(dataSource);
using var conn = dataSource.OpenConnection();
using var cmd = new Npgsql.NpgsqlCommand(
"""
INSERT INTO reputation_delta_audit (
id, player_id, faction_id, delta_amount, resulting_standing, source_kind, source_id, recorded_at)
VALUES (@id, @pid, @fid, @delta, @result, @kind, @source_id, @recorded)
ON CONFLICT (id) DO NOTHING;
""",
conn);
cmd.Parameters.AddWithValue("id", normalized.Id);
cmd.Parameters.AddWithValue("pid", normalized.PlayerId);
cmd.Parameters.AddWithValue("fid", normalized.FactionId);
cmd.Parameters.AddWithValue("delta", normalized.DeltaAmount);
cmd.Parameters.AddWithValue("result", normalized.ResultingStanding);
cmd.Parameters.AddWithValue("kind", normalized.SourceKind);
cmd.Parameters.AddWithValue("source_id", normalized.SourceId);
cmd.Parameters.AddWithValue("recorded", normalized.RecordedAt);
return cmd.ExecuteNonQuery() == 1;
}
/// <inheritdoc />
public IReadOnlyList<ReputationDeltaRow> GetDeltasForPlayer(string playerId, int? limit = null)
{
var player = FactionStandingIds.NormalizePlayerId(playerId);
if (player.Length == 0)
{
return [];
}
PostgresReputationDeltaBootstrap.EnsureSchema(dataSource);
using var conn = dataSource.OpenConnection();
var sql = limit is > 0
? """
SELECT id, player_id, faction_id, delta_amount, resulting_standing, source_kind, source_id, recorded_at
FROM reputation_delta_audit
WHERE player_id = @pid
ORDER BY recorded_at ASC, id ASC
LIMIT @lim;
"""
: """
SELECT id, player_id, faction_id, delta_amount, resulting_standing, source_kind, source_id, recorded_at
FROM reputation_delta_audit
WHERE player_id = @pid
ORDER BY recorded_at ASC, id ASC;
""";
using var cmd = new Npgsql.NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("pid", player);
if (limit is > 0)
{
cmd.Parameters.AddWithValue("lim", limit.Value);
}
var rows = new List<ReputationDeltaRow>();
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
rows.Add(new ReputationDeltaRow(
reader.GetString(0),
reader.GetString(1),
reader.GetString(2),
reader.GetInt32(3),
reader.GetInt32(4),
reader.GetString(5),
reader.GetString(6),
reader.GetFieldValue<DateTimeOffset>(7)));
}
return rows;
}
/// <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;
}
PostgresReputationDeltaBootstrap.EnsureSchema(dataSource);
using var conn = dataSource.OpenConnection();
using var cmd = new Npgsql.NpgsqlCommand(
"""
DELETE FROM reputation_delta_audit
WHERE player_id = @pid
AND source_kind = @kind
AND source_id = @source_id;
""",
conn);
cmd.Parameters.AddWithValue("pid", player);
cmd.Parameters.AddWithValue("kind", ReputationDeltaSourceKinds.QuestCompletion);
cmd.Parameters.AddWithValue("source_id", sourceId);
cmd.ExecuteNonQuery();
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;
}