Skip to content

Commit

Permalink
Hopefully fix kicking sometimes kicking the kicker (looped over array…
Browse files Browse the repository at this point in the history
… while modifying it, nono). Removed some warning causing code too
  • Loading branch information
DoubleF3lix committed Aug 30, 2024
1 parent da5334f commit 5f6dacc
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 66 deletions.
2 changes: 1 addition & 1 deletion Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public void onDisconnect() {
}
}

public string ToString() {
public override string ToString() {
return $"Client {{ ID = \"{this.ID}\" }}";
}
}
Expand Down
2 changes: 1 addition & 1 deletion GameData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public List<Player> getActivePlayers() {
}

public Player getPlayerByUsername(string username) {
return this.players.Find(player => player.username == username);
return this.players.Find(player => player.username == username) ?? new Player("");
}

// Remove player from the turnOrder list, adjusting the index backwards if necessary to avoid influencing order
Expand Down
20 changes: 9 additions & 11 deletions Player.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
namespace liveorlive_server {
// Internal server representation of player data
public class Player {
public class Player(string username, bool inGame = false, bool isSpectator = false) {
public const int DEFAULT_LIVES = 5;

public string username;
public bool inGame;
public bool isSpectator;
public string username = username;
public bool inGame = inGame;
public bool isSpectator = isSpectator;

public int lives = DEFAULT_LIVES;
public List<Item> items = new List<Item>(4);
public bool isSkipped = false;

public readonly long joinTime = (long)DateTime.UtcNow.Subtract(DateTime.UnixEpoch).TotalSeconds;

public Player(string username, bool inGame = false, bool isSpectator = false) {
this.username = username;
this.inGame = inGame; // This is necessary since player objects persist after their associated client disconnects
this.isSpectator = isSpectator;
}

public void setItems(List<Item> items) {
this.items = items;
}
Expand All @@ -31,8 +25,12 @@ public bool Equals(Player player) {
return this.username == player.username;
}

public string ToString() {
public override string ToString() {
return $"Player {{ username = \"{this.username}\", lives = {this.lives}, inGame = {this.inGame}, isSpectator = {this.isSpectator}, isSkipped = {this.isSkipped}, items = [{string.Join(", ", this.items)}] }}";
}

public override int GetHashCode() {
throw new NotImplementedException();
}
}
}
120 changes: 69 additions & 51 deletions Server.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ public class Server {

List<Client> connectedClients = new List<Client>();
GameData gameData = new GameData();
public GameLog gameLog = new GameLog();
public Chat chat = new Chat();
// TODO store GameLog in its own class as well (can still be stored in redux I guess)
GameLog gameLog = new GameLog();
Chat chat = new Chat();

public Server() {
this.app = WebApplication.CreateBuilder().Build();
Expand All @@ -39,11 +38,16 @@ public async Task start(string url, int port) {
// All clients are held in here, and this function only exits when they disconnect
private async Task ClientConnection(WebSocket webSocket, string ID) {
// Sometimes, clients can get stuck in a bugged closed state. This will attempt to purge them.
// Two phase to avoid looping over while removing it
List<Client> clientsToRemove = new List<Client>();
foreach (Client c in this.connectedClients) {
if (c.webSocket.State == WebSocketState.Closed) {
this.connectedClients.Remove(c);
clientsToRemove.Add(c);
}
}
foreach (Client c in clientsToRemove) {
this.connectedClients.Remove(c);
}

Client client = new Client(webSocket, this, ID);
this.connectedClients.Add(client);
Expand All @@ -58,48 +62,53 @@ private async Task ClientConnection(WebSocket webSocket, string ID) {
if (result.MessageType == WebSocketMessageType.Text) {
// Decode it to an object and pass it off
string message = Encoding.UTF8.GetString(buffer, 0, result.Count);
ClientPacket packet = JsonConvert.DeserializeObject<ClientPacket>(message, new PacketJSONConverter());
ClientPacket packet = JsonConvert.DeserializeObject<ClientPacket>(message, new PacketJSONConverter())!;
await this.packetReceived(client, packet);
} else if (result.MessageType == WebSocketMessageType.Close || webSocket.State == WebSocketState.Aborted) {
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
await webSocket.CloseAsync(
result.CloseStatus.HasValue ? result.CloseStatus.Value : WebSocketCloseStatus.InternalServerError,
result.CloseStatusDescription, CancellationToken.None);
break;
}
}
} catch (WebSocketException exception) {
} catch (WebSocketException) {
// Abormal disconnection, finally block has us covered
} finally {
this.connectedClients.Remove(client);
await this.handleClientDisconnect(client);
client.onDisconnect();
}
}

// If they're the host, try to pass that status on to someone else
// If they don't have a player assigned, don't bother
if (client.player?.username == this.gameData.host) {
// TODO null error here (client is made null while iterating over it?)
if (this.connectedClients.Count(client => client.player != null) > 0) {
Player newHost = this.connectedClients[0].player;
this.gameData.host = newHost.username;
await this.broadcast(new HostSetPacket { username = this.gameData.host });
} else {
this.gameData.host = null;
}
public async Task handleClientDisconnect(Client client) {
// If they're the host, try to pass that status on to someone else
// If they don't have a player assigned, don't bother
if (client.player?.username == this.gameData.host) {
if (this.connectedClients.Count(client => client.player != null) > 0) {
// Guarunteed to exist due to above condition, safe to use !
Player newHost = this.connectedClients[0].player!;
this.gameData.host = newHost.username;
await this.broadcast(new HostSetPacket { username = this.gameData.host });
} else {
this.gameData.host = null;
}
}

// If the game hasn't started, just remove them entirely
if (!this.gameData.gameStarted) {
if (client.player != null) {
this.gameData.players.Remove(this.gameData.getPlayerByUsername(client.player.username));
await this.syncGameData();
}
} else {
// If there is only one actively connected player and the game is in progress, end it
if (this.connectedClients.Where(client => client.player != null).Count() <= 1) {
await Console.Out.WriteLineAsync("Everyone has left the game. Ending with no winner.");
await this.endGame();
// If the game hasn't started, just remove them entirely
if (!this.gameData.gameStarted) {
if (client.player != null) {
this.gameData.players.Remove(this.gameData.getPlayerByUsername(client.player.username));
await this.syncGameData();
}
} else {
// If there is only one actively connected player and the game is in progress, end it
if (this.connectedClients.Where(client => client.player != null).Count() <= 1) {
await Console.Out.WriteLineAsync("Everyone has left the game. Ending with no winner.");
await this.endGame();
// Otherwise, if the current turn left, make them forfeit their turn
} else if (client.player != null && client.player.username == this.gameData.currentTurn) {
await this.forfeitTurn(client.player);
}
} else if (client.player != null && client.player.username == this.gameData.currentTurn) {
await this.forfeitTurn(client.player);
}
client.onDisconnect();
}
}

Expand Down Expand Up @@ -185,20 +194,26 @@ public async Task packetReceived(Client sender, ClientPacket packet) {
}

// Search for the correct client and kick them
foreach (Client client in this.connectedClients) {
if (client.player != null && client.player.username == kickPlayerPacket.username) {
Player target = client.player;
this.gameData.eliminatePlayer(target); // Needed to handle turn order
this.gameData.players.Remove(target); // We don't need them anymore

// Send currentTurn to avoid a game data sync (otherwise UI doesn't work properly)
await this.broadcast(new PlayerKickedPacket { username = client.player.username, currentTurn = this.gameData.currentTurn });
await this.sendGameLogMessage($"{target.username} has been kicked.");

// Actually DC them, which runs the close-block in ClientConnection and ensures the game ends if it needs to and what not
await client.webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "playerKicked", new CancellationToken());
}
Client? clientToKick = this.connectedClients.Find(client => client.player != null && client.player.username == kickPlayerPacket.username);
// Ignore so we don't crash trying to kick a ghost player
if (clientToKick == null || clientToKick.player == null) {
return;
}
Player target = clientToKick.player;
// End this players turn (checks for game end)
await this.postActionTransition(true);
// Eliminate them to handle adjusting turn order (have to do this after otherwise we skip two players)
this.gameData.eliminatePlayer(target);
// Discard the player entirely since they're likely not welcome back
this.gameData.players.Remove(target);

// Send currentTurn to avoid a game data sync (otherwise UI doesn't work properly)
await this.broadcast(new PlayerKickedPacket { username = target.username, currentTurn = this.gameData.currentTurn });
await this.sendGameLogMessage($"{target.username} has been kicked.");

// Actually disconnected them, which runs handleClientDisconnect
// This removes the client from connectedClients, and checks for game end or host transference (though host transfer should never occur on kick since the host cannot kick themselves)
await clientToKick.webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "playerKicked", new CancellationToken());
}
break;
case ShootPlayerPacket shootPlayerPacket:
Expand Down Expand Up @@ -256,6 +271,7 @@ public async Task packetReceived(Client sender, ClientPacket packet) {
if (sender.player == this.gameData.getCurrentPlayerForTurn()) {
if (sender.player.items.Remove(Item.Rebalancer)) {
int count = this.gameData.addAmmoToChamberAndShuffle(useRebalancerItemPacket.ammoType);
await this.broadcast(new RebalancerItemUsedPacket { ammoType = useRebalancerItemPacket.ammoType, count = count });
await this.sendGameLogMessage($"{sender.player.username} has used a Rebalancer item and added {count} {useRebalancerItemPacket.ammoType} rounds.");
} else {
await sender.sendMessage(new ActionFailedPacket { reason = "You don't have a Rebalancer item!" });
Expand Down Expand Up @@ -370,6 +386,9 @@ public async Task startGame() {
this.gameData.startGame();
await this.broadcast(new GameStartedPacket());

this.gameLog.clear();
await this.broadcast(new GameLogMessagesSyncPacket { messages = this.gameLog.getMessages() });

await this.startNewRound();
await this.nextTurn();
}
Expand Down Expand Up @@ -407,12 +426,14 @@ await this.broadcast(new NewRoundStartedPacket {

public async Task endGame() {
// There's almost certainly at least one player left when this runs (used to just wipe if there was nobody left, but that situation requires 2 people to DC at once which I don't care enough to account for)
string winner = this.gameData.players.Find(player => player.lives >= 1).username;
Player? possibleWinner = this.gameData.players.Find(player => player.lives >= 1);
string winner = possibleWinner != null ? possibleWinner.username : "nobody";

// Copy any data we may need (like players)
GameData newGameData = new GameData {
players = this.gameData.players.Where(player => player.inGame).Select(player => {
player.isSpectator = false;
player.isSkipped = false;
player.items.Clear();
player.lives = Player.DEFAULT_LIVES;
return player;
Expand All @@ -422,15 +443,12 @@ public async Task endGame() {
this.gameData = newGameData;
await this.syncGameData();

this.gameLog.clear();
await this.broadcast(new GameLogMessagesSyncPacket { messages = this.gameLog.getMessages() });
await this.sendGameLogMessage($"The game has ended. The winner is {winner}.");
}

public Client? getClientForCurrentTurn() {
Player currentPlayer = this.gameData.getCurrentPlayerForTurn();
Client currentClient = this.connectedClients.Find(client => client.player == currentPlayer);
return currentClient;
return this.connectedClients.Find(client => client.player == currentPlayer);
}

public async Task syncGameData() {
Expand Down
4 changes: 2 additions & 2 deletions TurnOrderManager.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
namespace liveorlive_server {
public class TurnOrderManager {
public string? currentTurn {
public string currentTurn {
get {
if (this.currentTurnIndex < 0) {
return null;
return "";
}
return this.turnOrder[this.currentTurnIndex];
}
Expand Down

0 comments on commit 5f6dacc

Please sign in to comment.