using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using Vanguard.Server.Configuration;
using Vanguard.Server.Storage;
namespace Vanguard.Server.Matches;
///
/// Регистр матчей: ровно один экземпляр grain на matchId.
/// В Orleans эту роль играет активация грана; контракт совпадает.
///
public sealed class MatchGrainRegistry
{
private readonly ConcurrentDictionary _matches = new(StringComparer.Ordinal);
private readonly IServiceProvider _services;
private readonly GameOptions _options;
private readonly IMatchStore _store;
private readonly ILogger _logger;
public MatchGrainRegistry(
IServiceProvider services,
GameOptions options,
IMatchStore store,
ILogger logger)
{
_services = services;
_options = options;
_store = store;
_logger = logger;
}
public async Task CreateAsync(MatchSetup setup)
{
var grain = ActivatorUtilities.CreateInstance(_services, setup);
if (!_matches.TryAdd(setup.MatchId, grain))
throw new InvalidOperationException($"Матч уже существует: {setup.MatchId}");
try
{
await _store.CreateAsync(grain.BuildHeader());
}
catch
{
_matches.TryRemove(setup.MatchId, out _);
throw;
}
_logger.LogInformation(
"Match {MatchId} created: {Mode}, {Player0} vs {Player1}.",
setup.MatchId, setup.Mode, setup.Seat0.PlayerId, setup.Seat1.PlayerId);
return grain;
}
/// Поднять матч после рестарта сервера из снапшота.
public async Task CreateRestoredAsync(MatchSnapshot snapshot)
{
var header = snapshot.Header;
var setup = new MatchSetup(
header.MatchId,
header.Mode,
new MatchSeatSetup(header.Player0Id, header.Deck0, 0),
new MatchSeatSetup(header.Player1Id, header.Deck1, 0),
header.Seed);
var grain = ActivatorUtilities.CreateInstance(_services, setup);
await grain.RestoreAsync(snapshot);
if (!_matches.TryAdd(header.MatchId, grain))
throw new InvalidOperationException($"Матч уже восстановлен: {header.MatchId}");
_logger.LogInformation(
"Match {MatchId} restored: {Mode}, {Player0} vs {Player1}, seq {Seq}.",
header.MatchId, header.Mode, header.Player0Id, header.Player1Id, header.EventSeq);
return grain;
}
public bool TryGet(string matchId, [NotNullWhen(true)] out IMatchGrain? match) =>
_matches.TryGetValue(matchId, out match);
public ICollection All => _matches.Values;
public int Count => _matches.Count;
public int ActiveCount => _matches.Values.Count(match => !match.Summary.IsFinished);
public void Remove(string matchId) => _matches.TryRemove(matchId, out _);
public void RemoveExpired(DateTimeOffset now)
{
var ttl = TimeSpan.FromSeconds(_options.CompletedMatchTtlSeconds);
foreach (var (matchId, match) in _matches)
{
var summary = match.Summary;
if (summary.IsFinished && summary.FinishedAt is { } finishedAt && now - finishedAt >= ttl)
{
_matches.TryRemove(matchId, out _);
_logger.LogInformation("Match {MatchId} removed after completion.", matchId);
}
}
}
}