using System.Diagnostics;
using ActionBlocks.Game;
using ActionBlocks.Server.Dtos;
using ActionBlocks.Server.Hubs;
using Microsoft.AspNetCore.SignalR;
namespace ActionBlocks.Server.Lobbies;
/// Advances every running match at a fixed rate and pushes snapshots to the lobby's clients.
public sealed class MatchRunner(LobbyManager lobbies, IHubContext hub, ILogger logger) : BackgroundService
{
private static readonly TimeSpan TickInterval = TimeSpan.FromMilliseconds(1000.0 / 60);
private static readonly TimeSpan ResultsDelay = TimeSpan.FromSeconds(4);
/// Snapshots are sent every n-th tick unless input made a lobby dirty.
private const int BroadcastEveryTicks = 2;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TickInterval);
var clock = Stopwatch.StartNew();
var last = clock.Elapsed;
long tick = 0;
while (await timer.WaitForNextTickAsync(stoppingToken))
{
var now = clock.Elapsed;
// Clamp to avoid huge jumps after a stall.
var delta = Math.Min((now - last).TotalSeconds, 0.1);
last = now;
tick++;
foreach (var lobby in lobbies.All)
{
try
{
Step(lobby, delta, tick % BroadcastEveryTicks == 0);
}
catch (Exception e)
{
logger.LogError(e, "Failed to update lobby {Code}", lobby.Code);
}
}
}
}
private void Step(Lobby lobby, double delta, bool broadcastTick)
{
MatchStateDto? snapshot = null;
LobbyDto? lobbyUpdate = null;
lock (lobby.Sync)
{
var match = lobby.Match;
if (match is null)
{
return;
}
match.Update(delta);
var events = match.DrainEvents();
if (broadcastTick || lobby.Dirty || events.Count > 0)
{
snapshot = SnapshotMapper.ToDto(match, events);
lobby.Dirty = false;
}
if (match.Phase == MatchPhase.Finished)
{
lobby.MatchFinishedAt ??= DateTimeOffset.UtcNow;
if (DateTimeOffset.UtcNow - lobby.MatchFinishedAt >= ResultsDelay)
{
lobby.EndMatch();
lobbyUpdate = lobby.ToDto();
}
}
}
if (snapshot is not null)
{
_ = hub.Clients.Group(lobby.Code).SendAsync(GameHub.MatchState, snapshot);
}
if (lobbyUpdate is not null)
{
_ = hub.Clients.Group(lobby.Code).SendAsync(GameHub.LobbyUpdated, lobbyUpdate);
}
}
}