VanguardReMaster/server/src/Vanguard.Server/Matches/Orleans/MatchActorGrain.cs

171 lines
5.2 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 Microsoft.Extensions.Logging;
using Orleans;
using Orleans.Concurrency;
using Vanguard.Rules.Cards;
using Vanguard.Server.Configuration;
using Vanguard.Server.Storage;
using Vanguard.Shared.Protocol;
namespace Vanguard.Server.Matches.Orleans;
/// <summary>
/// Единственная активная копия матча. Доменная логика остаётся в
/// <see cref="MatchGrain"/>; этот класс отвечает за активацию, восстановление
/// из хранилища и сериализуемую границу Orleans.
///
/// [KeepAlive] важен: если Orleans выгрузит грана, живые WebSocket-соединения
/// останутся в реестре без владельца. Долгоживущие матчи и так ограничены TTL,
/// а после рестарта сервера поднимаются из снапшота.
/// </summary>
[KeepAlive]
public sealed class MatchActorGrain : Grain, IMatchActor
{
private readonly CardCatalog _catalog;
private readonly GameOptions _options;
private readonly TimeProvider _time;
private readonly IMatchResultSink _results;
private readonly IMatchStore _store;
private readonly IMatchConnectionRegistry _connections;
private readonly ILogger<MatchGrain> _logger;
private string _matchId = string.Empty;
private MatchGrain? _match;
public MatchActorGrain(
CardCatalog catalog,
GameOptions options,
TimeProvider time,
IMatchResultSink results,
IMatchStore store,
IMatchConnectionRegistry connections,
ILogger<MatchGrain> logger)
{
_catalog = catalog;
_options = options;
_time = time;
_results = results;
_store = store;
_connections = connections;
_logger = logger;
}
public override async Task OnActivateAsync(CancellationToken cancellationToken)
{
_matchId = this.GetPrimaryKeyString();
// Новый матч ещё не в хранилище: активация просто оставляет его
// неинициализированным до вызова InitializeAsync из регистратуры.
var snapshot = await _store.GetAsync(_matchId);
if (snapshot is not null)
await RestoreInternalAsync(snapshot);
await base.OnActivateAsync(cancellationToken);
}
public async Task InitializeAsync(MatchSetupData setup)
{
if (_match is not null)
return;
var setupDomain = setup.ToSetup();
_match = new MatchGrain(
setupDomain,
_catalog,
_options,
_time,
_results,
_store,
_connections,
_logger);
await _store.CreateAsync(_match.BuildHeader());
}
public async Task EnsureInitializedAsync()
{
if (_match is not null)
return;
var snapshot = await _store.GetAsync(_matchId);
if (snapshot is null)
throw new InvalidOperationException($"Матч {_matchId} не найден в хранилище.");
await RestoreInternalAsync(snapshot);
}
public async Task<bool> AttachAsync(int seat, string playerId, long resumeFromSeq)
{
var match = await RequireAsync();
return await match.AttachAsync(seat, playerId, resumeFromSeq);
}
public async Task DetachAsync(int seat)
{
var match = await RequireAsync();
await match.DetachAsync(seat);
}
public async Task LeaveAsync(int seat)
{
var match = await RequireAsync();
await match.LeaveAsync(seat);
}
public async Task SubmitAsync(int seat, byte[] intentPayload)
{
var match = await RequireAsync();
var frame = ProtocolSerializer.DecodeClient(intentPayload);
if (frame is not ClientIntent intent)
throw new InvalidOperationException("Ожидался ClientIntent.");
await match.SubmitAsync(seat, intent);
}
public async Task ResyncAsync(int seat, long fromSeq, bool fullSnapshot)
{
var match = await RequireAsync();
await match.ResyncAsync(seat, fromSeq, fullSnapshot);
}
public async Task TickAsync(long nowUnixMs)
{
var match = await RequireAsync();
await match.TickAsync(DateTimeOffset.FromUnixTimeMilliseconds(nowUnixMs));
}
public async Task<MatchSummaryData> GetSummaryAsync()
{
var match = await RequireAsync();
return MatchSummaryData.From(match.Summary);
}
public async Task<bool> HasPlayerAsync(string playerId)
{
var match = await RequireAsync();
return match.HasPlayer(playerId);
}
private async Task<MatchGrain> RequireAsync()
{
await EnsureInitializedAsync();
return _match!;
}
private async Task RestoreInternalAsync(MatchSnapshot snapshot)
{
var setup = MatchSetup.FromHeader(snapshot.Header);
_match = new MatchGrain(
setup,
_catalog,
_options,
_time,
_results,
_store,
_connections,
_logger);
await _match.RestoreAsync(snapshot);
}
}