125 lines
4.6 KiB
C#
125 lines
4.6 KiB
C#
using System.Collections.Concurrent;
|
|
using Microsoft.Extensions.Options;
|
|
using NeonSprawl.Server.Game.PositionState;
|
|
|
|
namespace NeonSprawl.Server.Game.Factions;
|
|
|
|
/// <summary>Thread-safe in-memory faction standing; seeds the configured dev player (NEO-135).</summary>
|
|
public sealed class InMemoryFactionStandingStore(
|
|
IOptions<GamePositionOptions> options,
|
|
IFactionDefinitionRegistry factionRegistry) : IFactionStandingStore
|
|
{
|
|
private readonly HashSet<string> knownPlayers = CreateKnownPlayers(options.Value);
|
|
|
|
private readonly ConcurrentDictionary<string, int> standingByKey = new(StringComparer.Ordinal);
|
|
|
|
private readonly ConcurrentDictionary<string, object> keyLocks = new(StringComparer.Ordinal);
|
|
|
|
private static HashSet<string> CreateKnownPlayers(GamePositionOptions o)
|
|
{
|
|
var id = FactionStandingIds.NormalizePlayerId(o.DevPlayerId);
|
|
if (id.Length == 0)
|
|
{
|
|
throw new InvalidOperationException("Game:DevPlayerId must be non-empty.");
|
|
}
|
|
|
|
return new HashSet<string>(StringComparer.OrdinalIgnoreCase) { id };
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public bool CanWritePlayer(string playerId)
|
|
{
|
|
var player = FactionStandingIds.NormalizePlayerId(playerId);
|
|
return player.Length > 0 && knownPlayers.Contains(player);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public FactionStandingReadOutcome TryGetStanding(string playerId, string factionId)
|
|
{
|
|
var player = FactionStandingIds.NormalizePlayerId(playerId);
|
|
var faction = FactionStandingIds.NormalizeFactionId(factionId);
|
|
if (player.Length == 0)
|
|
{
|
|
return DenyRead(FactionStandingReasonCodes.PlayerNotWritable);
|
|
}
|
|
|
|
if (faction.Length == 0)
|
|
{
|
|
return DenyRead(FactionStandingReasonCodes.UnknownFaction);
|
|
}
|
|
|
|
if (!CanWritePlayer(player))
|
|
{
|
|
return DenyRead(FactionStandingReasonCodes.PlayerNotWritable);
|
|
}
|
|
|
|
if (!factionRegistry.TryGetDefinition(faction, out var definition) || definition is null)
|
|
{
|
|
return DenyRead(FactionStandingReasonCodes.UnknownFaction);
|
|
}
|
|
|
|
var key = FactionStandingIds.MakeStandingKey(player, faction);
|
|
var raw = standingByKey.GetValueOrDefault(key, 0);
|
|
return new FactionStandingReadOutcome(true, null, FactionStandingIds.ClampStanding(raw, definition));
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public FactionStandingMutationOutcome TryApplyStandingDelta(string playerId, string factionId, int deltaAmount)
|
|
{
|
|
var player = FactionStandingIds.NormalizePlayerId(playerId);
|
|
var faction = FactionStandingIds.NormalizeFactionId(factionId);
|
|
if (player.Length == 0)
|
|
{
|
|
return DenyMutation(FactionStandingReasonCodes.PlayerNotWritable);
|
|
}
|
|
|
|
if (faction.Length == 0)
|
|
{
|
|
return DenyMutation(FactionStandingReasonCodes.UnknownFaction);
|
|
}
|
|
|
|
if (!CanWritePlayer(player))
|
|
{
|
|
return DenyMutation(FactionStandingReasonCodes.PlayerNotWritable);
|
|
}
|
|
|
|
if (!factionRegistry.TryGetDefinition(faction, out var definition) || definition is null)
|
|
{
|
|
return DenyMutation(FactionStandingReasonCodes.UnknownFaction);
|
|
}
|
|
|
|
var key = FactionStandingIds.MakeStandingKey(player, faction);
|
|
lock (keyLocks.GetOrAdd(key, _ => new object()))
|
|
{
|
|
var raw = standingByKey.GetValueOrDefault(key, 0);
|
|
var previousStanding = FactionStandingIds.ClampStanding(raw, definition);
|
|
var newStanding = FactionStandingIds.ClampStanding(raw + deltaAmount, definition);
|
|
standingByKey[key] = newStanding;
|
|
return new FactionStandingMutationOutcome(true, null, previousStanding, newStanding);
|
|
}
|
|
}
|
|
|
|
private static FactionStandingReadOutcome DenyRead(string reasonCode) =>
|
|
new(false, reasonCode, 0);
|
|
|
|
private static FactionStandingMutationOutcome DenyMutation(string reasonCode) =>
|
|
new(false, reasonCode, 0, 0);
|
|
|
|
/// <summary>Test-only seed of raw standing before read-clamp (NEO-135 unit tests).</summary>
|
|
internal void SeedRawStandingForTests(string playerId, string factionId, int rawStanding)
|
|
{
|
|
var player = FactionStandingIds.NormalizePlayerId(playerId);
|
|
var faction = FactionStandingIds.NormalizeFactionId(factionId);
|
|
if (player.Length == 0 || faction.Length == 0 || !CanWritePlayer(player))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var key = FactionStandingIds.MakeStandingKey(player, faction);
|
|
lock (keyLocks.GetOrAdd(key, _ => new object()))
|
|
{
|
|
standingByKey[key] = rawStanding;
|
|
}
|
|
}
|
|
}
|