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; } if (!activeInstanceByPlayer.TryGetValue(player, out var instanceId)) { return false; } lock (instanceLocks.GetOrAdd(PlayerLockKey(player), _ => new object())) { if (!byInstanceId.TryGetValue(instanceId, out var row) || row.Status != ContractInstanceStatus.Active || !string.Equals(row.PlayerId, player, StringComparison.Ordinal)) { activeInstanceByPlayer.TryRemove(player, out _); 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(player, out _); } 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(player, out _); snapshot = row.ToSnapshot(); return true; } } } /// 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 false; } _ = byInstanceId.TryRemove(instanceId, out _); if (row.Status == ContractInstanceStatus.Active) { activeInstanceByPlayer.TryRemove(player, out _); } return true; } } } }