neon-sprawl/server/NeonSprawl.Server/Game/Encounters/InMemoryEncounterProgressSt...

106 lines
3.3 KiB
C#

using System.Collections.Concurrent;
namespace NeonSprawl.Server.Game.Encounters;
/// <summary>Thread-safe in-memory encounter progress; empty at startup (NEO-104).</summary>
public sealed class InMemoryEncounterProgressStore : IEncounterProgressStore
{
private sealed class ProgressRow(bool started, HashSet<string> defeatedNpcInstanceIds)
{
public bool Started { get; } = started;
public HashSet<string> DefeatedNpcInstanceIds { get; } = defeatedNpcInstanceIds;
}
private readonly ConcurrentDictionary<string, ProgressRow> byKey = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, object> keyLocks = new(StringComparer.Ordinal);
/// <inheritdoc />
public bool TryGetProgress(string playerId, string encounterId, out EncounterProgressSnapshot snapshot)
{
snapshot = default!;
var key = EncounterProgressIds.MakeProgressKey(playerId, encounterId);
if (key.Length == 0)
{
return false;
}
lock (keyLocks.GetOrAdd(key, _ => new object()))
{
if (!byKey.TryGetValue(key, out var row))
{
return false;
}
snapshot = new EncounterProgressSnapshot(
EncounterProgressIds.NormalizePlayerId(playerId),
EncounterProgressIds.NormalizeEncounterId(encounterId),
row.Started,
new HashSet<string>(row.DefeatedNpcInstanceIds, StringComparer.Ordinal));
return true;
}
}
/// <inheritdoc />
public bool TryActivate(string playerId, string encounterId)
{
var key = EncounterProgressIds.MakeProgressKey(playerId, encounterId);
if (key.Length == 0)
{
return false;
}
lock (keyLocks.GetOrAdd(key, _ => new object()))
{
if (byKey.TryGetValue(key, out var existing) && existing.Started)
{
return false;
}
byKey[key] = new ProgressRow(
started: true,
defeatedNpcInstanceIds: new HashSet<string>(StringComparer.Ordinal));
return true;
}
}
/// <inheritdoc />
public bool TryAddDefeatedTarget(string playerId, string encounterId, string npcInstanceId)
{
var key = EncounterProgressIds.MakeProgressKey(playerId, encounterId);
var npcKey = EncounterProgressIds.NormalizeNpcInstanceId(npcInstanceId);
if (key.Length == 0 || npcKey.Length == 0)
{
return false;
}
lock (keyLocks.GetOrAdd(key, _ => new object()))
{
if (!byKey.TryGetValue(key, out var row) || !row.Started)
{
return false;
}
return row.DefeatedNpcInstanceIds.Add(npcKey);
}
}
/// <summary>Dev fixture only — removes one player+encounter progress row (NEO-108 Bruno reset).</summary>
public bool TryClear(string playerId, string encounterId)
{
var key = EncounterProgressIds.MakeProgressKey(playerId, encounterId);
if (key.Length == 0)
{
return false;
}
lock (keyLocks.GetOrAdd(key, _ => new object()))
{
var removed = byKey.TryRemove(key, out _);
keyLocks.TryRemove(key, out _);
return removed;
}
}
}