Added username validation

Auto hide enter/leave room
Username starts as UNNAMED now
Show roomnumber for connected room
This commit is contained in:
Michael Chen 2022-11-04 15:13:56 +01:00
parent 391b5569a4
commit 531c0e1344
Signed by: cnml
GPG Key ID: 5845BF3F82D5F629
6 changed files with 113 additions and 53 deletions

View File

@ -21,7 +21,6 @@ public class PongHub : Hub<IPongClient> {
public override Task OnConnectedAsync() { public override Task OnConnectedAsync() {
Player = Lobby.CreatePlayer(); Player = Lobby.CreatePlayer();
Player.Client = Clients.Client(Context.ConnectionId); Player.Client = Clients.Client(Context.ConnectionId);
Player.Username = "Anon";
return Task.CompletedTask; return Task.CompletedTask;
} }
@ -42,10 +41,10 @@ public class PongHub : Hub<IPongClient> {
return Task.FromResult(room.ID); return Task.FromResult(room.ID);
} }
public Task<string> JoinRoom(string roomId) { public async Task<string> JoinRoom(string roomId) {
AssertNotInRoom(); AssertNotInRoom();
var room = Lobby.JoinRoom(Player, roomId); var room = await Lobby.JoinRoom(Player, roomId);
return Task.FromResult(room.ID); return room.ID;
} }
public Task MovePaddle(int dir) { public Task MovePaddle(int dir) {
@ -53,21 +52,14 @@ public class PongHub : Hub<IPongClient> {
var direction = (PongPaddleDirection)dir; var direction = (PongPaddleDirection)dir;
if (!Enum.IsDefined(direction)) if (!Enum.IsDefined(direction))
throw new HubException($"Invalid direction: {dir}!"); throw new HubException($"Invalid direction: {dir}!");
room.MovePaddle(Player, direction); return room.MovePaddle(Player, direction);
return Task.CompletedTask;
} }
public Task LeaveRoom() { public Task LeaveRoom()
Lobby.LeaveRoom(Player); => Lobby.LeaveRoom(Player);
return Task.CompletedTask;
}
public Task RequestUsernameChange(string username) { public Task RequestUsernameChange(string username)
// TOOD: check this => Lobby.ChangeUsername(Player, username);
Logger.LogInformation("Player {Player} requested username change to [{username}]", Player, username);
Player.Username = username;
return Task.CompletedTask;
}
public override Task OnDisconnectedAsync(Exception? exception) { public override Task OnDisconnectedAsync(Exception? exception) {
Lobby.RemovePlayer(Player); Lobby.RemovePlayer(Player);

View File

@ -1,4 +1,6 @@
using Microsoft.AspNetCore.SignalR; using System.Numerics;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.SignalR;
namespace PongGame.Hubs; namespace PongGame.Hubs;
@ -35,19 +37,24 @@ public class PongLobby {
return room; return room;
} }
public PongRoom JoinRoom(PongPlayer player, string roomId) { public Task<PongRoom> JoinRoom(PongPlayer player, string roomId) {
PongRoom? room; PongRoom? room;
lock (PongRooms) { lock (PongRooms) {
room = PongRooms.GetValueOrDefault(roomId); room = PongRooms.GetValueOrDefault(roomId);
} }
if (room is null) throw new HubException($"Room [{roomId}] not found!"); if (room is null) throw new HubException($"Room [{roomId}] not found!");
room.Join(player); room.Join(player);
return room; return Task.FromResult(room);
} }
public void LeaveRoom(PongPlayer player) { public Task LeaveRoom(PongPlayer player) {
if (player.ConnectedRoom is PongRoom room) if (player.ConnectedRoom is PongRoom room) {
room.Leave(player); room.Leave(player);
if (room.IsEmpty)
lock (PongRooms)
_ = PongRooms.Remove(room.ID);
}
return Task.CompletedTask;
} }
private readonly Random random = new(); private readonly Random random = new();
@ -61,4 +68,28 @@ public class PongLobby {
} while (PongRooms.ContainsKey(id)); } while (PongRooms.ContainsKey(id));
return 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;
}
} }

View File

@ -4,7 +4,7 @@ namespace PongGame.Hubs;
[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")] [DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")]
public class PongPlayer { public class PongPlayer {
private string username = default!; private string username = "UNNAMED";
public PongRoom? ConnectedRoom { get; internal set; } public PongRoom? ConnectedRoom { get; internal set; }
public string Username { public string Username {

View File

@ -18,6 +18,8 @@ public class PongRoom {
public PongPlayer? Player1 { get; private set; } public PongPlayer? Player1 { get; private set; }
public PongPlayer? Player2 { get; private set; } public PongPlayer? Player2 { get; private set; }
public bool IsEmpty => Player1 is null && Player2 is null;
private PongGameState State = PongGameState.Initial; private PongGameState State = PongGameState.Initial;
public void Join(PongPlayer player) { public void Join(PongPlayer player) {
@ -67,7 +69,7 @@ public class PongRoom {
if (Player1 is PongPlayer player1 && Player2 is PongPlayer player2) { if (Player1 is PongPlayer player1 && Player2 is PongPlayer player2) {
Logger.LogInformation("[{ID}] Pong game started: {player1} vs. {player2}", ID, player1, player2); Logger.LogInformation("[{ID}] Pong game started: {player1} vs. {player2}", ID, player1, player2);
ResumeGame(); ResumeGame();
} else if (Player1 is null && Player2 is null) { } else if (IsEmpty) {
CloseRoom(); CloseRoom();
} else } else
PauseGame(); PauseGame();
@ -84,16 +86,15 @@ public class PongRoom {
public override string ToString() => $"[{ID}]"; public override string ToString() => $"[{ID}]";
public void MovePaddle(PongPlayer player, PongPaddleDirection direction) { public Task MovePaddle(PongPlayer player, PongPaddleDirection direction) {
if (Player1 == player) { if (Player1 == player) {
State.Paddle1.Direction = direction; State.Paddle1.Direction = direction;
Logger.LogDebug(DIRECTION_LOG_TEMPLATE, ID, 1, player, direction); Logger.LogDebug(DIRECTION_LOG_TEMPLATE, ID, 1, player, direction);
return;
} else if (Player2 == player) { } else if (Player2 == player) {
State.Paddle2.Direction = direction; State.Paddle2.Direction = direction;
Logger.LogDebug(DIRECTION_LOG_TEMPLATE, ID, 2, player, direction); Logger.LogDebug(DIRECTION_LOG_TEMPLATE, ID, 2, player, direction);
return; } else
}
throw new InvalidOperationException("Player is not in this room, but moved! Assumably players room wasn't deleted."); throw new InvalidOperationException("Player is not in this room, but moved! Assumably players room wasn't deleted.");
return Task.CompletedTask;
} }
} }

View File

@ -8,21 +8,25 @@
<h1 class="display-4">Pong</h1> <h1 class="display-4">Pong</h1>
<h3 id="connection">Connection Status</h3> <h3 id="connection">Connection Status</h3>
<button id="createlobby" class="btn btn-primary mb-3">Create</button> <button id="createlobby" class="btn btn-primary mb-3">Create Room</button>
<button id="leavelobby" class="btn btn-primary mb-3">Leave</button> <button id="leavelobby" class="btn btn-primary mb-3 d-none">Leave Room</button>
<div class="input-group mb-3"> <div id="joinroomdiv" class="input-group mb-3">
<input id="roomid" type="text" class="form-control" placeholder="Room ID" aria-label="Room ID" aria-describedby="joinroom"> <input id="roomid" type="text" class="form-control" placeholder="Room ID" aria-label="Room ID" aria-describedby="joinroom">
<button id="joinroom" class="btn btn-outline-secondary" type="button">Button</button> <button id="joinroom" class="btn btn-outline-secondary" type="button">Join Room</button>
</div> </div>
<div class="input-group mb-3"> <div class="input-group mb-3 has-validation">
<input id="username" type="text" class="form-control" placeholder="Username" aria-label="Username" aria-describedby="setusername"> <input id="username" type="text" class="form-control" placeholder="Username" aria-label="Username" aria-describedby="setusername">
<button id="setusername" class="btn btn-outline-secondary" type="button">Set Username</button> <button id="setusername" class="btn btn-outline-secondary" type="button">Set Username</button>
<div id="usernameerror" class="invalid-feedback"></div>
</div> </div>
<div>
<h4>Room <span id="connectedroomid"></span></h4>
<div id="canvas-container" class="mb-3"></div> <div id="canvas-container" class="mb-3"></div>
</div> </div>
</div>
<script src="~/lib/signalr/dist/browser/signalr.min.js"></script> <script src="~/lib/signalr/dist/browser/signalr.min.js"></script>
<script src="~/lib/signalr/dist/browser/signalr-protocol-msgpack.min.js"></script> <script src="~/lib/signalr/dist/browser/signalr-protocol-msgpack.min.js"></script>

View File

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