using System.Collections.Concurrent; using Microsoft.Extensions.Options; using NeonSprawl.Server.Game.PositionState; namespace NeonSprawl.Server.Game.Contracts; /// Thread-safe in-memory contract instances; seeds the configured dev player (NEO-146). public sealed class InMemoryContractInstanceStore(IOptions options) : IContractInstanceStore { private sealed class InstanceRow( string contractInstanceId, string templateId, string playerId, ContractInstanceStatus status, string seedBucket, DateTimeOffset issuedAt, DateTimeOffset? completedAt) { public string ContractInstanceId { get; } = contractInstanceId; public string TemplateId { get; } = templateId; public string PlayerId { get; } = playerId; public ContractInstanceStatus Status { get; private set; } = status; public string SeedBucket { get; } = seedBucket; public DateTimeOffset IssuedAt { get; } = issuedAt; public DateTimeOffset? CompletedAt { get; private set; } = completedAt; public ContractInstanceState ToSnapshot() => new(ContractInstanceId, TemplateId, PlayerId, Status, SeedBucket, IssuedAt, CompletedAt); public void MarkCompleted(DateTimeOffset completedAt) { Status = ContractInstanceStatus.Completed; CompletedAt = completedAt; } } private readonly HashSet knownPlayers = CreateKnownPlayers(options.Value); private readonly ConcurrentDictionary byInstanceId = new(StringComparer.Ordinal); private readonly ConcurrentDictionary activeInstanceByPlayer = new(StringComparer.Ordinal); private readonly ConcurrentDictionary instanceLocks = new(StringComparer.Ordinal); private static string PlayerLockKey(string player) => $"{player}\0active"; private static HashSet CreateKnownPlayers(GamePositionOptions o) { var id = ContractInstanceIds.NormalizePlayerId(o.DevPlayerId); if (id.Length == 0) { throw new InvalidOperationException("Game:DevPlayerId must be non-empty."); } return new HashSet(StringComparer.OrdinalIgnoreCase) { id }; } /// public bool CanWritePlayer(string playerId) { var player = ContractInstanceIds.NormalizePlayerId(playerId); return player.Length > 0 && knownPlayers.Contains(player); } /// public bool TryGet(string playerId, string contractInstanceId, out ContractInstanceState snapshot) { snapshot = null!; var player = ContractInstanceIds.NormalizePlayerId(playerId); var instanceId = ContractInstanceIds.NormalizeContractInstanceId(contractInstanceId); if (player.Length == 0 || instanceId.Length == 0 || !knownPlayers.Contains(player)) { return false; } var lockKey = ContractInstanceIds.MakeInstanceKey(player, instanceId); if (lockKey.Length == 0) { return false; } lock (instanceLocks.GetOrAdd(lockKey, _ => new object())) { if (!byInstanceId.TryGetValue(instanceId, out var row) || !string.Equals(row.PlayerId, player, StringComparison.Ordinal)) { return false; } snapshot = row.ToSnapshot(); return true; } } /// public bool TryGetActiveForPlayer(string playerId, out ContractInstanceState snapshot) { snapshot = null!; var player = ContractInstanceIds.NormalizePlayerId(playerId); if (player.Length == 0 || !knownPlayers.Contains(player)) { return false; } lock (instanceLocks.GetOrAdd(PlayerLockKey(player), _ => new object())) { if (!activeInstanceByPlayer.TryGetValue(player, out var instanceId)) { return false; } if (!byInstanceId.TryGetValue(instanceId, out var row) || row.Status != ContractInstanceStatus.Active || !string.Equals(row.PlayerId, player, StringComparison.Ordinal)) { activeInstanceByPlayer.TryRemove(new KeyValuePair(player, instanceId)); return false; } snapshot = row.ToSnapshot(); return true; } } /// public bool TryCreateActive( string playerId, string contractInstanceId, string templateId, string seedBucket, DateTimeOffset issuedAt, out ContractInstanceState snapshot) { snapshot = null!; var player = ContractInstanceIds.NormalizePlayerId(playerId); var instanceId = ContractInstanceIds.NormalizeContractInstanceId(contractInstanceId); var normalizedTemplateId = ContractInstanceIds.NormalizeTemplateId(templateId); var normalizedSeedBucket = ContractInstanceIds.NormalizeSeedBucket(seedBucket); if (player.Length == 0 || instanceId.Length == 0 || normalizedTemplateId.Length == 0 || normalizedSeedBucket.Length == 0 || !knownPlayers.Contains(player)) { return false; } lock (instanceLocks.GetOrAdd(PlayerLockKey(player), _ => new object())) { lock (instanceLocks.GetOrAdd(ContractInstanceIds.MakeInstanceKey(player, instanceId), _ => new object())) { if (activeInstanceByPlayer.TryGetValue(player, out var existingActiveId)) { if (byInstanceId.TryGetValue(existingActiveId, out var existingActive) && existingActive.Status == ContractInstanceStatus.Active) { snapshot = existingActive.ToSnapshot(); return false; } activeInstanceByPlayer.TryRemove(new KeyValuePair(player, existingActiveId)); } if (byInstanceId.TryGetValue(instanceId, out var existing)) { snapshot = existing.ToSnapshot(); return false; } var row = new InstanceRow( instanceId, normalizedTemplateId, player, ContractInstanceStatus.Active, normalizedSeedBucket, issuedAt, null); byInstanceId[instanceId] = row; activeInstanceByPlayer[player] = instanceId; snapshot = row.ToSnapshot(); return true; } } } /// public bool TryMarkComplete( string playerId, string contractInstanceId, DateTimeOffset completedAt, out ContractInstanceState snapshot) { snapshot = null!; var player = ContractInstanceIds.NormalizePlayerId(playerId); var instanceId = ContractInstanceIds.NormalizeContractInstanceId(contractInstanceId); if (player.Length == 0 || instanceId.Length == 0 || !knownPlayers.Contains(player)) { return false; } var lockKey = ContractInstanceIds.MakeInstanceKey(player, instanceId); if (lockKey.Length == 0) { return false; } lock (instanceLocks.GetOrAdd(PlayerLockKey(player), _ => new object())) { lock (instanceLocks.GetOrAdd(lockKey, _ => new object())) { if (!byInstanceId.TryGetValue(instanceId, out var row) || !string.Equals(row.PlayerId, player, StringComparison.Ordinal)) { return false; } if (row.Status == ContractInstanceStatus.Completed) { snapshot = row.ToSnapshot(); return false; } if (row.Status != ContractInstanceStatus.Active) { snapshot = row.ToSnapshot(); return false; } row.MarkCompleted(completedAt); activeInstanceByPlayer.TryRemove(new KeyValuePair(player, instanceId)); snapshot = row.ToSnapshot(); return true; } } } /// public IReadOnlyList ListForPlayer(string playerId, int maxCompletedRows) { var player = ContractInstanceIds.NormalizePlayerId(playerId); if (player.Length == 0 || !knownPlayers.Contains(player) || maxCompletedRows < 0) { return []; } lock (instanceLocks.GetOrAdd(PlayerLockKey(player), _ => new object())) { ContractInstanceState? active = null; if (activeInstanceByPlayer.TryGetValue(player, out var activeId) && byInstanceId.TryGetValue(activeId, out var activeRow) && activeRow.Status == ContractInstanceStatus.Active && string.Equals(activeRow.PlayerId, player, StringComparison.Ordinal)) { active = activeRow.ToSnapshot(); } var completed = new List(); foreach (var row in byInstanceId.Values) { if (!string.Equals(row.PlayerId, player, StringComparison.Ordinal) || row.Status != ContractInstanceStatus.Completed) { continue; } completed.Add(row.ToSnapshot()); } completed.Sort(static (a, b) => { var cmp = b.IssuedAt.CompareTo(a.IssuedAt); return cmp != 0 ? cmp : string.CompareOrdinal(a.ContractInstanceId, b.ContractInstanceId); }); if (completed.Count > maxCompletedRows) { completed.RemoveRange(maxCompletedRows, completed.Count - maxCompletedRows); } if (active is null) { return completed; } var result = new List(1 + completed.Count) { active }; result.AddRange(completed); return result; } } /// public bool TryClearInstance(string playerId, string contractInstanceId) { var player = ContractInstanceIds.NormalizePlayerId(playerId); var instanceId = ContractInstanceIds.NormalizeContractInstanceId(contractInstanceId); if (player.Length == 0 || instanceId.Length == 0 || !knownPlayers.Contains(player)) { return false; } var lockKey = ContractInstanceIds.MakeInstanceKey(player, instanceId); if (lockKey.Length == 0) { return false; } lock (instanceLocks.GetOrAdd(PlayerLockKey(player), _ => new object())) { lock (instanceLocks.GetOrAdd(lockKey, _ => new object())) { if (!byInstanceId.TryGetValue(instanceId, out var row) || !string.Equals(row.PlayerId, player, StringComparison.Ordinal)) { return true; } _ = byInstanceId.TryRemove(instanceId, out _); if (row.Status == ContractInstanceStatus.Active) { activeInstanceByPlayer.TryRemove(new KeyValuePair(player, instanceId)); } return true; } } } }