neon-sprawl/server/NeonSprawl.Server/Game/Factions/ReputationOperations.cs

98 lines
3.7 KiB
C#

namespace NeonSprawl.Server.Game.Factions;
/// <summary>
/// Orchestrates auditable faction standing apply: standing mutation + append-only audit row (NEO-136).
/// Quest reward wiring: <see cref="Rewards.RewardRouterOperations"/> (NEO-138).
/// NEO-141 telemetry hook site: successful apply path in <see cref="TryApplyDelta"/>.
/// </summary>
public static class ReputationOperations
{
/// <summary>
/// Applies a signed standing delta for one player+faction, clamps to faction bounds, and appends one audit row.
/// Callers must not invoke <see cref="IFactionStandingStore.TryApplyStandingDelta"/> alone for game apply.
/// </summary>
public static ReputationApplyOutcome TryApplyDelta(
string playerId,
string factionId,
int deltaAmount,
string deltaId,
string sourceKind,
string sourceId,
IFactionStandingStore standingStore,
IReputationDeltaStore auditStore,
TimeProvider timeProvider)
{
if (deltaAmount == 0)
{
return Deny(ReputationApplyReasonCodes.InvalidDelta);
}
var normalizedPlayerId = FactionStandingIds.NormalizePlayerId(playerId);
var normalizedFactionId = FactionStandingIds.NormalizeFactionId(factionId);
var normalizedDeltaId = deltaId.Trim();
if (normalizedDeltaId.Length == 0)
{
return Deny(ReputationApplyReasonCodes.InvalidDeltaId);
}
var normalizedSourceKind = sourceKind.Trim();
var normalizedSourceId = sourceId.Trim();
if (normalizedSourceKind.Length == 0 || normalizedSourceId.Length == 0)
{
return Deny(ReputationApplyReasonCodes.InvalidSource);
}
var mutation = standingStore.TryApplyStandingDelta(normalizedPlayerId, normalizedFactionId, deltaAmount);
if (!mutation.Success)
{
return new ReputationApplyOutcome(
false,
mutation.ReasonCode,
mutation.PreviousStanding,
mutation.NewStanding,
null);
}
var appliedDelta = mutation.NewStanding - mutation.PreviousStanding;
var row = new ReputationDeltaRow(
normalizedDeltaId,
normalizedPlayerId,
normalizedFactionId,
appliedDelta,
mutation.NewStanding,
normalizedSourceKind,
normalizedSourceId,
timeProvider.GetUtcNow());
if (auditStore.TryAppend(row))
{
// --- Telemetry hook site (NEO-141): future E9.M1 catalog event `reputation_delta` ---
// TODO(E9.M1): catalog emit — once per successful auditable apply (standing + audit committed).
// Not on deny paths, audit append failure + compensating revert, or caller rollback paths.
// Planned payload fields: playerId, factionId, appliedDelta, newStanding, sourceKind, sourceId, deltaId, appliedAt.
// No ingest or ILogger here (comments-only).
return new ReputationApplyOutcome(
true,
null,
mutation.PreviousStanding,
mutation.NewStanding,
row);
}
if (appliedDelta != 0)
{
_ = standingStore.TryApplyStandingDelta(normalizedPlayerId, normalizedFactionId, -appliedDelta);
}
return new ReputationApplyOutcome(
false,
ReputationApplyReasonCodes.AuditAppendFailed,
mutation.PreviousStanding,
mutation.PreviousStanding,
null);
}
private static ReputationApplyOutcome Deny(string reasonCode) =>
new(false, reasonCode, 0, 0, null);
}