using ActionBlocks.Game; using ActionBlocks.Server.Dtos; using ActionBlocks.Server.Lobbies; using Microsoft.AspNetCore.SignalR; namespace ActionBlocks.Server.Hubs; /// /// Client-facing API. Server-to-client messages: "LobbyUpdated" (LobbyDto) and "MatchState" (MatchStateDto). /// public sealed class GameHub(LobbyManager lobbies, GameSettings settings) : Hub { public const string LobbyUpdated = "LobbyUpdated"; public const string MatchState = "MatchState"; public async Task CreateLobby(string name) { var (lobby, player) = Run(() => lobbies.Create(Context.ConnectionId, name)); return await Joined(lobby, player); } public async Task JoinLobby(string code, string name) { var (lobby, player) = Run(() => lobbies.Join(Context.ConnectionId, code, name)); return await Joined(lobby, player); } public async Task LeaveLobby() { var lobby = lobbies.FindByConnection(Context.ConnectionId); if (lobby is null) { return; } await Groups.RemoveFromGroupAsync(Context.ConnectionId, lobby.Code); await LeaveAndNotify(); } public async Task StartGame() { var lobby = lobbies.FindByConnection(Context.ConnectionId) ?? throw new HubException("You are not in a lobby."); LobbyDto dto; lock (lobby.Sync) { var player = lobby.FindByConnection(Context.ConnectionId); if (player is null || player.Id != lobby.HostId) { throw new HubException("Only the host can start the game."); } if (lobby.IsPlaying) { throw new HubException("A game is already running."); } lobby.StartMatch(settings); dto = lobby.ToDto(); } await Clients.Group(lobby.Code).SendAsync(LobbyUpdated, dto); } public void Input(InputAction action) => WithPlayer((lobby, player) => { lobby.Match?.HandleInput(player.Id, action); lobby.Dirty = true; }); public void SelectTarget(string targetId) => WithPlayer((lobby, player) => lobby.Match?.SelectTarget(player.Id, targetId)); public IReadOnlyList GetItems() => Game.Items.ItemRegistry.Default.All.Select(i => new ItemInfoDto(i.Id, i.DisplayName)).ToList(); public override async Task OnDisconnectedAsync(Exception? exception) { await LeaveAndNotify(); await base.OnDisconnectedAsync(exception); } private async Task Joined(Lobby lobby, LobbyPlayer player) { await Groups.AddToGroupAsync(Context.ConnectionId, lobby.Code); LobbyDto dto; lock (lobby.Sync) { dto = lobby.ToDto(); } await Clients.OthersInGroup(lobby.Code).SendAsync(LobbyUpdated, dto); return new JoinResultDto(player.Id, dto); } private async Task LeaveAndNotify() { var lobby = lobbies.Leave(Context.ConnectionId); if (lobby is null) { return; } LobbyDto dto; lock (lobby.Sync) { lobby.Dirty = true; dto = lobby.ToDto(); } await Clients.Group(lobby.Code).SendAsync(LobbyUpdated, dto); } private void WithPlayer(Action action) { var lobby = lobbies.FindByConnection(Context.ConnectionId); if (lobby is null) { return; } lock (lobby.Sync) { if (lobby.FindByConnection(Context.ConnectionId) is { } player) { action(lobby, player); } } } private static T Run(Func action) { try { return action(); } catch (LobbyException e) { throw new HubException(e.Message); } } }