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