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 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;
}
}
}
}