106 lines
3.6 KiB
C#
106 lines
3.6 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|
||
}
|