diff --git a/PongGame/Hubs/PongHub.cs b/PongGame/Hubs/PongHub.cs index d5d77da..ff5c48d 100644 --- a/PongGame/Hubs/PongHub.cs +++ b/PongGame/Hubs/PongHub.cs @@ -21,7 +21,6 @@ public class PongHub : Hub { public override Task OnConnectedAsync() { Player = Lobby.CreatePlayer(); Player.Client = Clients.Client(Context.ConnectionId); - Player.Username = "Anon"; return Task.CompletedTask; } @@ -42,10 +41,10 @@ public class PongHub : Hub { return Task.FromResult(room.ID); } - public Task JoinRoom(string roomId) { + public async Task JoinRoom(string roomId) { AssertNotInRoom(); - var room = Lobby.JoinRoom(Player, roomId); - return Task.FromResult(room.ID); + var room = await Lobby.JoinRoom(Player, roomId); + return room.ID; } public Task MovePaddle(int dir) { @@ -53,21 +52,14 @@ public class PongHub : Hub { var direction = (PongPaddleDirection)dir; if (!Enum.IsDefined(direction)) throw new HubException($"Invalid direction: {dir}!"); - room.MovePaddle(Player, direction); - return Task.CompletedTask; + return room.MovePaddle(Player, direction); } - public Task LeaveRoom() { - Lobby.LeaveRoom(Player); - return Task.CompletedTask; - } + public Task LeaveRoom() + => Lobby.LeaveRoom(Player); - public Task RequestUsernameChange(string username) { - // TOOD: check this - Logger.LogInformation("Player {Player} requested username change to [{username}]", Player, username); - Player.Username = username; - return Task.CompletedTask; - } + public Task RequestUsernameChange(string username) + => Lobby.ChangeUsername(Player, username); public override Task OnDisconnectedAsync(Exception? exception) { Lobby.RemovePlayer(Player); diff --git a/PongGame/Hubs/PongLobbyCollection.cs b/PongGame/Hubs/PongLobbyCollection.cs index 73e601e..9a7bc6a 100644 --- a/PongGame/Hubs/PongLobbyCollection.cs +++ b/PongGame/Hubs/PongLobbyCollection.cs @@ -1,4 +1,6 @@ -using Microsoft.AspNetCore.SignalR; +using System.Numerics; +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.SignalR; namespace PongGame.Hubs; @@ -35,19 +37,24 @@ public class PongLobby { return room; } - public PongRoom JoinRoom(PongPlayer player, string roomId) { + public Task JoinRoom(PongPlayer player, string roomId) { PongRoom? room; lock (PongRooms) { room = PongRooms.GetValueOrDefault(roomId); } if (room is null) throw new HubException($"Room [{roomId}] not found!"); room.Join(player); - return room; + return Task.FromResult(room); } - public void LeaveRoom(PongPlayer player) { - if (player.ConnectedRoom is PongRoom room) + public Task LeaveRoom(PongPlayer player) { + if (player.ConnectedRoom is PongRoom room) { room.Leave(player); + if (room.IsEmpty) + lock (PongRooms) + _ = PongRooms.Remove(room.ID); + } + return Task.CompletedTask; } private readonly Random random = new(); @@ -61,4 +68,28 @@ public class PongLobby { } while (PongRooms.ContainsKey(id)); return id; } + + public async Task ChangeUsername(PongPlayer player, string username) { + username = ValidateUsername(username); + if (player.Username == username) { + await player.Client.UsernameChanged(username); + return; + } + lock (connectedPlayers) { + // TODO: separate hashset for usernames + if (connectedPlayers.Select(i => i.Username).Contains(username)) + throw new HubException($"Username {username} is already taken!"); + } + Logger.LogInformation("Player {Player} requested username change to [{username}]", player, username); + player.Username = username; + } + + private static readonly Regex UsernameRegex = new(@"^(?!.*[._ -]{2})[\w._ -]{3,20}$", RegexOptions.Compiled, TimeSpan.FromMilliseconds(200)); + + private static string ValidateUsername(string username) { + username = username.Trim(); + if (!UsernameRegex.IsMatch(username)) + throw new HubException($"At most 20 characters, no two consecutive symbols"); + return username; + } } \ No newline at end of file diff --git a/PongGame/Hubs/PongPlayer.cs b/PongGame/Hubs/PongPlayer.cs index ab3c9d3..b8e6a4d 100644 --- a/PongGame/Hubs/PongPlayer.cs +++ b/PongGame/Hubs/PongPlayer.cs @@ -4,7 +4,7 @@ namespace PongGame.Hubs; [DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")] public class PongPlayer { - private string username = default!; + private string username = "UNNAMED"; public PongRoom? ConnectedRoom { get; internal set; } public string Username { diff --git a/PongGame/Hubs/PongRoom.cs b/PongGame/Hubs/PongRoom.cs index ac8932d..b6153e7 100644 --- a/PongGame/Hubs/PongRoom.cs +++ b/PongGame/Hubs/PongRoom.cs @@ -18,6 +18,8 @@ public class PongRoom { public PongPlayer? Player1 { get; private set; } public PongPlayer? Player2 { get; private set; } + public bool IsEmpty => Player1 is null && Player2 is null; + private PongGameState State = PongGameState.Initial; public void Join(PongPlayer player) { @@ -67,7 +69,7 @@ public class PongRoom { if (Player1 is PongPlayer player1 && Player2 is PongPlayer player2) { Logger.LogInformation("[{ID}] Pong game started: {player1} vs. {player2}", ID, player1, player2); ResumeGame(); - } else if (Player1 is null && Player2 is null) { + } else if (IsEmpty) { CloseRoom(); } else PauseGame(); @@ -84,16 +86,15 @@ public class PongRoom { public override string ToString() => $"[{ID}]"; - public void MovePaddle(PongPlayer player, PongPaddleDirection direction) { + public Task MovePaddle(PongPlayer player, PongPaddleDirection direction) { if (Player1 == player) { State.Paddle1.Direction = direction; Logger.LogDebug(DIRECTION_LOG_TEMPLATE, ID, 1, player, direction); - return; } else if (Player2 == player) { State.Paddle2.Direction = direction; Logger.LogDebug(DIRECTION_LOG_TEMPLATE, ID, 2, player, direction); - return; - } - throw new InvalidOperationException("Player is not in this room, but moved! Assumably players room wasn't deleted."); + } else + throw new InvalidOperationException("Player is not in this room, but moved! Assumably players room wasn't deleted."); + return Task.CompletedTask; } } \ No newline at end of file diff --git a/PongGame/Pages/Pong.cshtml b/PongGame/Pages/Pong.cshtml index 0a4d17b..acdf8ef 100644 --- a/PongGame/Pages/Pong.cshtml +++ b/PongGame/Pages/Pong.cshtml @@ -8,20 +8,24 @@

Pong

Connection Status

- - + + -
+
- +
-
+
+
-
+
+

Room

+
+
diff --git a/PongGame/wwwroot/js/pong.js b/PongGame/wwwroot/js/pong.js index e6b0c0a..216e903 100644 --- a/PongGame/wwwroot/js/pong.js +++ b/PongGame/wwwroot/js/pong.js @@ -11,26 +11,29 @@ function getElement(id) { } const connectionStatus = getElement("connection"); -const createlobby = getElement("createlobby"); +const createroom = getElement("createlobby"); const roomidinput = getElement("roomid"); const joinroom = getElement("joinroom"); +const joinroomdiv = getElement("joinroomdiv"); const usernameinput = getElement("username"); const setusername = getElement("setusername"); -const leavelobby = getElement("leavelobby"); +const leaveroom = getElement("leavelobby"); +const usernameerror = getElement("usernameerror"); +const connectedroomid = getElement("connectedroomid"); -connection.onclose(function (error) { - if (error) { +connection.onclose(function (err) { + if (err) { connectionStatus.textContent = "Unexpected error!"; - return console.error(`Connection aborted: ${error.message}`); + return console.error(`Connection aborted: ${err.message}`); } console.info("Disconnected!"); connectionStatus.textContent = "Closed!"; }); -connection.onreconnecting(function (error) { - if (error) { +connection.onreconnecting(function (err) { + if (err) { connectionStatus.textContent = "Reconnecting!"; - return console.error(`Connection reconnecting: ${error.message}`); + return console.error(`Connection reconnecting: ${err.message}`); } console.info("Reconnecting!"); connectionStatus.textContent = "Reconnecting!"; @@ -41,11 +44,20 @@ connection.onreconnected(function (connectionId) { connectionStatus.textContent = "Connected!"; }); -createlobby.addEventListener("click", function (event) { - connection.invoke("CreateRoom").then(function (roomId) { - roomidinput.value = roomId; - console.info(`Joined room [${roomId}]`); - }).catch(function (err) { +function show(elem) { elem.classList.remove("d-none"); } +function hide(elem) { elem.classList.add("d-none"); } + +function roomJoined(roomId) { + roomidinput.value = roomId; + connectedroomid.textContent = roomId; + console.info(`Joined room [${roomId}]`); + hide(createroom); + hide(joinroomdiv); + show(leaveroom); +} + +createroom.addEventListener("click", function (event) { + connection.invoke("CreateRoom").then(roomJoined).catch(function (err) { return console.error(err.toString()); }); event.preventDefault(); @@ -57,24 +69,44 @@ connection.on("GameStateChanged", function (state) { connection.on("UsernameChanged", function (username) { console.info(`Username is now ${username}`); usernameinput.value = username; + usernameinput.classList.remove("is-invalid"); + usernameinput.classList.add("is-valid"); }); joinroom.addEventListener("click", function (event) { - connection.invoke("JoinRoom", roomidinput.value).catch(function (err) { + connection.invoke("JoinRoom", roomidinput.value).then(roomJoined).catch(function (err) { return console.error(err.toString()); }); event.preventDefault(); }); +function hubExceptionMessage(msg) { + const needle = "HubException: "; + let idx = msg.lastIndexOf(needle); + if (idx < 0) return msg; + return msg.substr(idx + needle.length); +} + +usernameinput.addEventListener("input", function (event) { + usernameinput.classList.remove("is-valid"); +}); + setusername.addEventListener("click", function (event) { connection.invoke("RequestUsernameChange", usernameinput.value).catch(function (err) { + usernameerror.textContent = hubExceptionMessage(err.message); + usernameinput.classList.add("is-invalid"); return console.error(err.toString()); }); event.preventDefault(); }); -leavelobby.addEventListener("click", function (event) { - connection.invoke("LeaveRoom").catch(function (err) { +leaveroom.addEventListener("click", function (event) { + connection.invoke("LeaveRoom").then(function () { + roomidinput.value = ""; + show(createroom); + show(joinroomdiv); + hide(leaveroom); + }).catch(function (err) { return console.error(err.toString()); }); event.preventDefault(); @@ -88,7 +120,7 @@ function movePaddle(direction) { // Create the application helper and add its render target to the page let app = new PIXI.Application({ width: 1000, height: 500 }); -getElement('canvas-container').appendChild(app.view); +getElement("canvas-container").appendChild(app.view); let graphics = new PIXI.Graphics(); app.stage.addChild(graphics); @@ -163,8 +195,8 @@ const keyEvent = (function () { return handler; })(); -document.addEventListener('keydown', keyEvent); -document.addEventListener('keyup', keyEvent); +document.addEventListener("keydown", keyEvent); +document.addEventListener("keyup", keyEvent); connection.start().then(function () { console.info(`Connected!`);