Added username validation
Auto hide enter/leave room Username starts as UNNAMED now Show roomnumber for connected room
This commit is contained in:
parent
391b5569a4
commit
531c0e1344
@ -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);
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -8,20 +8,24 @@
|
|||||||
<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>
|
||||||
|
@ -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!`);
|
||||||
|
Loading…
Reference in New Issue
Block a user