From 63bb7cd7800e647bd5690c1b1550cca832865545 Mon Sep 17 00:00:00 2001 From: Mahmoud Raafat <100778020+MahmoudRafaat@users.noreply.github.com> Date: Mon, 12 Jan 2026 16:56:32 +0200 Subject: [PATCH 1/4] Handle player disconnects and add leaderboard broadcast Added methods in GameServerManager to broadcast the leaderboard and retrieve leaderboard messages. Implemented handlePlayerDisconnect in GameManager to clean up pending requests and active games when a player disconnects, notifying opponents and updating player statuses accordingly. Updated removePlayer in GameServerManager to invoke this disconnect handler. --- .../domain/server/GameServerManager.java | 37 +++++++-- .../domain/services/game/GameManager.java | 75 ++++++++++++++++++- 2 files changed, 105 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/mycompany/tictactoeserver/domain/server/GameServerManager.java b/src/main/java/com/mycompany/tictactoeserver/domain/server/GameServerManager.java index 25e6a9c..c1bfe35 100644 --- a/src/main/java/com/mycompany/tictactoeserver/domain/server/GameServerManager.java +++ b/src/main/java/com/mycompany/tictactoeserver/domain/server/GameServerManager.java @@ -1,11 +1,12 @@ package com.mycompany.tictactoeserver.domain.server; - import com.google.gson.Gson; import com.mycompany.tictactoeserver.datasource.model.Player; import com.mycompany.tictactoeserver.domain.entity.PlayerEntity; import com.mycompany.tictactoeserver.domain.entity.PlayerStatus; import com.mycompany.tictactoeserver.domain.services.communication.*; import com.mycompany.tictactoeserver.domain.services.communication.MessageRouter; +import com.mycompany.tictactoeserver.domain.services.game.GameManager; +import com.mycompany.tictactoeserver.domain.services.statistics.StatisticsService; import com.mycompany.tictactoeserver.domain.utils.callbacks.PlayerHandlerCallback; import com.mycompany.tictactoeserver.domain.utils.exception.ExceptionHandlerMiddleware; import com.mycompany.tictactoeserver.domain.utils.exception.PlayerSendMessageException; @@ -14,8 +15,8 @@ import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; +import java.util.List; import java.util.Vector; - public class GameServerManager { private final Vector players = new Vector<>(); public Thread thread; @@ -62,6 +63,34 @@ public Vector getAvailablePlayerData() { } } return available; +} + public Message getLeaderboardMessage() { + StatisticsService statsService = new StatisticsService(); + + List topPlayersList = statsService.getLeaderboard(); + + Vector leaderboardVector = new Vector<>(topPlayersList); + + AvailablePlayersInfo info = new AvailablePlayersInfo(leaderboardVector, null, null); + + return Message.createMessage(MessageType.RESPONSE, Action.GET_LEADERBOARD, info); +} + +public void broadcastLeaderboard() { + Message msg = getLeaderboardMessage(); + String jsonMsg = gson.toJson(msg); + + synchronized (lock) { + for (PlayerConnectionHandler player : players) { + try { + if (player.getPlayer() != null) { + player.sendMessageToPlayer(jsonMsg); + } + } catch (Exception e) { + System.out.println("Error broadcasting leaderboard: " + e.getMessage()); + } + } + } } public Message getAvailablePlayersMessage(PlayerConnectionHandler requester) { Vector online = new Vector<>(); @@ -90,15 +119,12 @@ public Message getAvailablePlayersMessage(PlayerConnectionHandler requester) { return Message.createMessage(MessageType.RESPONSE, Action.GET_AVAILABLE_PLAYERS, info); } - - public void start() { synchronized (lock) { System.out.println("Starting GameServerManager"); runnable.toggleRunning(); } } - public void stop() { System.out.println("Stopping GameServerManager"); runnable.toggleRunning(); @@ -159,6 +185,7 @@ public void addPlayer(PlayerConnectionHandler player) { } public void removePlayer(PlayerConnectionHandler player) { + GameManager.getInstance().handlePlayerDisconnect(player); synchronized (lock) { players.remove(player); } diff --git a/src/main/java/com/mycompany/tictactoeserver/domain/services/game/GameManager.java b/src/main/java/com/mycompany/tictactoeserver/domain/services/game/GameManager.java index 0a4eb57..f9ea1c3 100644 --- a/src/main/java/com/mycompany/tictactoeserver/domain/services/game/GameManager.java +++ b/src/main/java/com/mycompany/tictactoeserver/domain/services/game/GameManager.java @@ -130,7 +130,11 @@ public Message handleGameResponse(GameResponseInfo requestInfo, PlayerConnection responder.setStatus(PlayerStatus.ONLINE); GameServerManager.getInstance().broadcastPlayerList(); - return Message.createMessage(MessageType.RESPONSE, Action.GAME_RESPONSE, requestInfo); + Message refusalMsg = Message.createMessage(MessageType.RESPONSE, Action.GAME_RESPONSE, requestInfo); + + requester.sendMessageToPlayer(gson.toJson(refusalMsg)); + + return null; } return startGame(requester, responder); @@ -202,7 +206,6 @@ public Message forwardMove(MoveInfo moveInfo) { if (opponent == null) { return Message.createMessage(MessageType.ERROR, Action.INVALID_OPPONENT, moveInfo); } - Message msg = Message.createMessage(MessageType.RESPONSE, Action.SEND_GAME_UPDATE, moveInfo); opponent.sendMessageToPlayer(gson.toJson(msg)); @@ -215,6 +218,74 @@ public Message forwardMove(MoveInfo moveInfo) { } } +public void handlePlayerDisconnect(PlayerConnectionHandler disconnectedPlayer) { + if (disconnectedPlayer == null) return; + + System.out.println("Handling disconnect for: " + + (disconnectedPlayer.getPlayer() != null ? disconnectedPlayer.getPlayer().getUsername() : "Unknown")); + + + GameRequest foundRequest = null; + synchronized (lock) { + for (GameRequest req : pendingRequests) { + if (req.requester.equals(disconnectedPlayer) || req.target.equals(disconnectedPlayer)) { + foundRequest = req; + break; + } + } + if (foundRequest != null) { + pendingRequests.remove(foundRequest); + } + } + + if (foundRequest != null) { + PlayerConnectionHandler opponent = (foundRequest.requester.equals(disconnectedPlayer)) + ? foundRequest.target + : foundRequest.requester; + + opponent.setStatus(PlayerStatus.ONLINE); + GameServerManager.getInstance().broadcastPlayerList(); + + try { + + Message msg = Message.createMessage(MessageType.ERROR, Action.INVALID_OPPONENT, null); + opponent.sendMessageToPlayer(gson.toJson(msg)); + } catch (Exception e) { + System.out.println("Failed to notify opponent of pending disconnect: " + e.getMessage()); + } + } + + + GameRoom foundRoom = null; + synchronized (lock) { + for (GameRoom room : activeRooms) { + if (room.getPlayer1().equals(disconnectedPlayer) || room.getPlayer2().equals(disconnectedPlayer)) { + foundRoom = room; + break; + } + } + if (foundRoom != null) { + activeRooms.remove(foundRoom); + } + } + + if (foundRoom != null) { + PlayerConnectionHandler survivor = (foundRoom.getPlayer1().equals(disconnectedPlayer)) + ? foundRoom.getPlayer2() + : foundRoom.getPlayer1(); + + survivor.setStatus(PlayerStatus.ONLINE); + GameServerManager.getInstance().broadcastPlayerList(); + + try { + Message msg = Message.createMessage(MessageType.ERROR, Action.INVALID_OPPONENT, null); + survivor.sendMessageToPlayer(gson.toJson(msg)); + } catch (Exception e) { + System.out.println("Failed to notify survivor of game disconnect: " + e.getMessage()); + } + } +} + } From ffcfe9af58cc37d6438a6cc18efa3d4a2d257b18 Mon Sep 17 00:00:00 2001 From: Mahmoud Raafat <100778020+MahmoudRafaat@users.noreply.github.com> Date: Mon, 12 Jan 2026 16:56:54 +0200 Subject: [PATCH 2/4] Add leaderboard broadcast and retrieval support Broadcasts the leaderboard after user registration and adds a GET_LEADERBOARD action to the Action enum. MessageRouter now handles GET_LEADERBOARD requests, and minor code cleanup was performed in authentication and game response handling. --- .../authentication/AuthenticationService.java | 14 +++++++++----- .../domain/services/communication/Action.java | 3 ++- .../services/communication/MessageRouter.java | 11 +++++++++-- .../domain/services/game/MoveInfo.java | 1 + 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/mycompany/tictactoeserver/domain/services/authentication/AuthenticationService.java b/src/main/java/com/mycompany/tictactoeserver/domain/services/authentication/AuthenticationService.java index c71f1a7..28c360b 100644 --- a/src/main/java/com/mycompany/tictactoeserver/domain/services/authentication/AuthenticationService.java +++ b/src/main/java/com/mycompany/tictactoeserver/domain/services/authentication/AuthenticationService.java @@ -61,6 +61,8 @@ public Message register(AuthRequestEntity credential) { AuthResponseEntity responseEntity = new AuthResponseEntity(newPlayer); response = Message.createMessage(MessageType.RESPONSE, Action.REGISTERATION_SUCCESS, responseEntity); + GameServerManager.getInstance().broadcastLeaderboard(); + } catch (HashingException ex) { ServerInterruptException customException = new ServerInterruptException(ex.getStackTrace()); @@ -68,6 +70,7 @@ public Message register(AuthRequestEntity credential) { response = Message.createMessage(MessageType.ERROR, Action.INTERNAL_SERVER_ERROR, credential); } + return response; } @@ -85,11 +88,7 @@ public Message login(AuthRequestEntity credential,PlayerConnectionHandler client response = Message.createMessage(MessageType.ERROR, Action.USERNAME_NOT_FOUND, credential); return response; } -if (clientSession != null) { - clientSession.setPlayer(player); - clientSession.setStatus(PlayerStatus.ONLINE); - } - GameServerManager.getInstance().broadcastPlayerList(); + String hashedPassword = ServerSecurityManager.hashText(credential.getPassword()); if (!hashedPassword.equals(player.getPassword())) { @@ -100,6 +99,11 @@ public Message login(AuthRequestEntity credential,PlayerConnectionHandler client AuthResponseEntity responseEntity = new AuthResponseEntity(player); response = Message.createMessage(MessageType.RESPONSE, Action.LOGIN_SUCCESS, responseEntity); + if (clientSession != null) { + clientSession.setPlayer(player); + clientSession.setStatus(PlayerStatus.ONLINE); + } + GameServerManager.getInstance().broadcastPlayerList(); } catch (HashingException ex) { ServerInterruptException customException = new ServerInterruptException(ex.getStackTrace()); diff --git a/src/main/java/com/mycompany/tictactoeserver/domain/services/communication/Action.java b/src/main/java/com/mycompany/tictactoeserver/domain/services/communication/Action.java index 070f54f..0f942ed 100644 --- a/src/main/java/com/mycompany/tictactoeserver/domain/services/communication/Action.java +++ b/src/main/java/com/mycompany/tictactoeserver/domain/services/communication/Action.java @@ -20,7 +20,8 @@ public enum Action { ROOM_NOT_FOUND(150), INVALID_OPPONENT(160), NO_PENDING_REQUEST(170), - GET_AVAILABLE_PLAYERS(180); + GET_AVAILABLE_PLAYERS(180), + GET_LEADERBOARD(200); final int id; diff --git a/src/main/java/com/mycompany/tictactoeserver/domain/services/communication/MessageRouter.java b/src/main/java/com/mycompany/tictactoeserver/domain/services/communication/MessageRouter.java index 14e7be2..50d997a 100644 --- a/src/main/java/com/mycompany/tictactoeserver/domain/services/communication/MessageRouter.java +++ b/src/main/java/com/mycompany/tictactoeserver/domain/services/communication/MessageRouter.java @@ -78,6 +78,7 @@ private void handleRequest(Message message , PlayerConnectionHandler sender) { case REGISTER -> { System.out.println("come reg"); + AuthRequestEntity auth = gson.fromJson(message.getData(), AuthRequestEntity.class); @@ -94,6 +95,10 @@ private void handleRequest(Message message , PlayerConnectionHandler sender) { Message response = GameManager.getInstance().requestGame(requestInfo, sender, target); sender.sendMessageToPlayer(gson.toJson(response)); +} + case GET_LEADERBOARD -> { + Message response = server.getLeaderboardMessage(); + sender.sendMessageToPlayer(gson.toJson(response)); } @@ -110,10 +115,12 @@ private void handleResponse(Message msg, PlayerConnectionHandler sender) throws switch (action) { case GAME_RESPONSE -> { + GameResponseInfo requestInfo = gson.fromJson(msg.getData(), GameResponseInfo.class); response = GameManager.getInstance().handleGameResponse(requestInfo, sender); - sender.sendMessageToPlayer(gson.toJson(response)); - } +if (response != null) { + sender.sendMessageToPlayer(gson.toJson(response)); + } } case SEND_GAME_UPDATE -> { MoveInfo moveInfo = gson.fromJson(msg.getData(), MoveInfo.class); diff --git a/src/main/java/com/mycompany/tictactoeserver/domain/services/game/MoveInfo.java b/src/main/java/com/mycompany/tictactoeserver/domain/services/game/MoveInfo.java index e47e410..cd6f7c4 100644 --- a/src/main/java/com/mycompany/tictactoeserver/domain/services/game/MoveInfo.java +++ b/src/main/java/com/mycompany/tictactoeserver/domain/services/game/MoveInfo.java @@ -12,6 +12,7 @@ public class MoveInfo { private final String roomId; private final String playerId; private final Object move; + public MoveInfo(String playerId, Object move, String roomId) { this.playerId = playerId; From 5dbee36bd25563439614fbaf080bc73eea0fe6a9 Mon Sep 17 00:00:00 2001 From: Mahmoud Raafat <100778020+MahmoudRafaat@users.noreply.github.com> Date: Mon, 12 Jan 2026 17:22:26 +0200 Subject: [PATCH 3/4] Refactor GameServerManager and minor cleanup Refactored GameServerManager by removing duplicate and misplaced code in getAvailablePlayersMessage, improving method structure and readability. --- .../domain/server/GameServerManager.java | 14 +++----------- .../authentication/AuthenticationService.java | 2 +- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/mycompany/tictactoeserver/domain/server/GameServerManager.java b/src/main/java/com/mycompany/tictactoeserver/domain/server/GameServerManager.java index 0388c74..04ff959 100644 --- a/src/main/java/com/mycompany/tictactoeserver/domain/server/GameServerManager.java +++ b/src/main/java/com/mycompany/tictactoeserver/domain/server/GameServerManager.java @@ -1,4 +1,5 @@ package com.mycompany.tictactoeserver.domain.server; + import com.google.gson.Gson; import com.mycompany.tictactoeserver.datasource.model.Player; import com.mycompany.tictactoeserver.domain.entity.PlayerEntity; @@ -68,8 +69,7 @@ public Vector getAvailablePlayerData() { } return available; } - return available; -} + public Message getLeaderboardMessage() { StatisticsService statsService = new StatisticsService(); @@ -98,17 +98,12 @@ public void broadcastLeaderboard() { } } } + public Message getAvailablePlayersMessage(PlayerConnectionHandler requester) { Vector online = new Vector<>(); Vector inGame = new Vector<>(); Vector pending = new Vector<>(); - - public Message getAvailablePlayersMessage(PlayerConnectionHandler requester) { - Vector online = new Vector<>(); - Vector inGame = new Vector<>(); - Vector pending = new Vector<>(); - synchronized (lock) { for (PlayerConnectionHandler handler : players) { if (handler.getPlayer() == null) { @@ -134,10 +129,7 @@ public Message getAvailablePlayersMessage(PlayerConnectionHandler requester) { AvailablePlayersInfo info = new AvailablePlayersInfo(online, inGame, pending); return Message.createMessage(MessageType.RESPONSE, Action.GET_AVAILABLE_PLAYERS, info); - } - AvailablePlayersInfo info = new AvailablePlayersInfo(online, inGame, pending); - return Message.createMessage(MessageType.RESPONSE, Action.GET_AVAILABLE_PLAYERS, info); } public void start() { diff --git a/src/main/java/com/mycompany/tictactoeserver/domain/services/authentication/AuthenticationService.java b/src/main/java/com/mycompany/tictactoeserver/domain/services/authentication/AuthenticationService.java index 0c8435f..3f119fd 100644 --- a/src/main/java/com/mycompany/tictactoeserver/domain/services/authentication/AuthenticationService.java +++ b/src/main/java/com/mycompany/tictactoeserver/domain/services/authentication/AuthenticationService.java @@ -118,7 +118,7 @@ public Message login(AuthRequestEntity credential, PlayerConnectionHandler clien } - } + } catch (HashingException ex) { ServerInterruptException customException = new ServerInterruptException(ex.getStackTrace()); ExceptionHandlerMiddleware.getInstance().handleException(customException); From c5cf0ffd706c2218ee3a09d8a6645aa71bcde1a5 Mon Sep 17 00:00:00 2001 From: Mahmoud Raafat <100778020+MahmoudRafaat@users.noreply.github.com> Date: Mon, 12 Jan 2026 18:08:14 +0200 Subject: [PATCH 4/4] Update AuthenticationService.java --- .../domain/services/authentication/AuthenticationService.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/com/mycompany/tictactoeserver/domain/services/authentication/AuthenticationService.java b/src/main/java/com/mycompany/tictactoeserver/domain/services/authentication/AuthenticationService.java index 3f119fd..5c9dc13 100644 --- a/src/main/java/com/mycompany/tictactoeserver/domain/services/authentication/AuthenticationService.java +++ b/src/main/java/com/mycompany/tictactoeserver/domain/services/authentication/AuthenticationService.java @@ -63,9 +63,7 @@ public Message register(AuthRequestEntity credential) { playerDao.insert(newPlayer); AuthResponseEntity responseEntity = new AuthResponseEntity(newPlayer); - response = Message.createMessage(MessageType.RESPONSE, Action.REGISTERATION_SUCCESS, responseEntity); - GameServerManager.getInstance().broadcastLeaderboard(); - + response = Message.createMessage(MessageType.RESPONSE, Action.REGISTERATION_SUCCESS, responseEntity); } catch (HashingException ex) { ServerInterruptException customException = new ServerInterruptException(ex.getStackTrace());