VanguardReMaster/server/src/Vanguard.Server/Matches/MatchGrainRegistry.cs

106 lines
3.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using Vanguard.Server.Configuration;
using Vanguard.Server.Storage;
namespace Vanguard.Server.Matches;
/// <summary>
/// Регистр матчей: ровно один экземпляр grain на matchId.
/// В Orleans эту роль играет активация грана; контракт совпадает.
/// </summary>
public sealed class MatchGrainRegistry
{
private readonly ConcurrentDictionary<string, IMatchGrain> _matches = new(StringComparer.Ordinal);
private readonly IServiceProvider _services;
private readonly GameOptions _options;
private readonly IMatchStore _store;
private readonly ILogger<MatchGrainRegistry> _logger;
public MatchGrainRegistry(
IServiceProvider services,
GameOptions options,
IMatchStore store,
ILogger<MatchGrainRegistry> logger)
{
_services = services;
_options = options;
_store = store;
_logger = logger;
}
public async Task<IMatchGrain> CreateAsync(MatchSetup setup)
{
var grain = ActivatorUtilities.CreateInstance<MatchGrain>(_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;
}
/// <summary>Поднять матч после рестарта сервера из снапшота.</summary>
public async Task<IMatchGrain> 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<MatchGrain>(_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<IMatchGrain> 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);
}
}
}
}