From ca4283bbe7c01eabbff501e0138a322aa11170d2 Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Sat, 11 Apr 2026 16:38:57 +0200 Subject: [PATCH 01/20] Feat: Add createLobby command on server side Refs #93 --- .../create_lobby/CreateLobbyHandler.java | 32 +++++++++++++++++++ .../lobby/create_lobby/CreateLobbyParser.java | 11 +++++++ .../create_lobby/CreateLobbyRequest.java | 10 ++++++ .../create_lobby/CreateLobbyResponse.java | 14 ++++++++ 4 files changed, 67 insertions(+) create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/create_lobby/CreateLobbyHandler.java create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/create_lobby/CreateLobbyParser.java create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/create_lobby/CreateLobbyRequest.java create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/create_lobby/CreateLobbyResponse.java diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/create_lobby/CreateLobbyHandler.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/create_lobby/CreateLobbyHandler.java new file mode 100644 index 0000000..b86b145 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/create_lobby/CreateLobbyHandler.java @@ -0,0 +1,32 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby; + +import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager; +import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher; + +public class CreateLobbyHandler extends CommandHandler { + private final LobbyManager lobbyManager; + + public CreateLobbyHandler(ResponseDispatcher responseDispatcher, LobbyManager lobbyManager) { + super(responseDispatcher); + this.lobbyManager = lobbyManager; + } + + @Override + public void execute(CreateLobbyRequest request) { + LobbyId id = lobbyManager.createNewLobby(null); + + if (id == null) { + responseDispatcher.dispatch( + new ErrorResponse( + request.getContext(), + "LOBBIES_FULL", + "Maximum number of 8 lobbies reached")); + return; + } + + responseDispatcher.dispatch(new CreateLobbyResponse(request.getContext(), id.value())); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/create_lobby/CreateLobbyParser.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/create_lobby/CreateLobbyParser.java new file mode 100644 index 0000000..00040f8 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/create_lobby/CreateLobbyParser.java @@ -0,0 +1,11 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest; + +public class CreateLobbyParser implements CommandParser { + @Override + public CreateLobbyRequest parse(PrimitiveRequest primitiveRequest) { + return new CreateLobbyRequest(primitiveRequest.context()); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/create_lobby/CreateLobbyRequest.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/create_lobby/CreateLobbyRequest.java new file mode 100644 index 0000000..669a0d0 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/create_lobby/CreateLobbyRequest.java @@ -0,0 +1,10 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext; + +public class CreateLobbyRequest extends Request { + public CreateLobbyRequest(RequestContext context) { + super(context); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/create_lobby/CreateLobbyResponse.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/create_lobby/CreateLobbyResponse.java new file mode 100644 index 0000000..cb7371f --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/create_lobby/CreateLobbyResponse.java @@ -0,0 +1,14 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder; + +/** Sends ID of newly created lobby back to client. */ +public class CreateLobbyResponse extends SuccessResponse { + public CreateLobbyResponse(RequestContext context, int lobbyId) { + super( + context, + new ResponseBodyBuilder().param("LOBBY_ID", String.valueOf(lobbyId)).build()); + } +} From 62f01d1e5da34c98b947cf94ecbdb6c7f27c4293 Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Sat, 11 Apr 2026 16:44:19 +0200 Subject: [PATCH 02/20] Docs: add documentation of create_lobby commad --- .../networking/commands/protocol-document.md | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/documents/docs/networking/commands/protocol-document.md b/documents/docs/networking/commands/protocol-document.md index 340dc8e..3f61212 100644 --- a/documents/docs/networking/commands/protocol-document.md +++ b/documents/docs/networking/commands/protocol-document.md @@ -453,4 +453,58 @@ GET_NEXT_MESSAGE +OK TYPE=GLOBAL GAME=-1 USER=player1 TARGET=null TIME=9:30 TEXT="Guten Tag" END +``` + +## CREATE_LOBBY command + +The `CREATE_LOBBY` command requests the server to create a new lobby and return its identifier. + +### Required pre-execution checks + +None. + +### Request Parameters + +No parameters. + +### Implementation notes + +- Parser: CreateLobbyParser — constructs a CreateLobbyRequest from the incoming PrimitiveRequest context (no parameters are read). +- Handler: CreateLobbyHandler — calls LobbyManager.createNewLobby(null). + - If the returned LobbyId is null, the handler dispatches an ErrorResponse with code `LOBBIES_FULL` and message "Maximum number of 8 lobbies reached". + - On success, the handler dispatches a CreateLobbyResponse containing the new lobby id. + +### Success Response + +| Field | Type | Description | +| :-------- | :--- | :---------- | +| `LOBBY_ID`| `int`| Numeric id of the newly created lobby | + +### Error Response + +| Code | Description | +| :---------- | :--------------------------------------------------------------------- | +| `LOBBIES_FULL` | The server cannot create a new lobby because the maximum number of lobbies has been reached | + +### Example Request + +``` +CREATE_LOBBY +``` + +### Example Success Response + +``` ++OK + LOBBY_ID=1 +END +``` + +### Example Error Response + +``` +-ERR + CODE=LOBBIES_FULL + MESSAGE=Maximum number of 8 lobbies reached +END ``` \ No newline at end of file From 23debb18bce23245f1277f94f73fe256ee43db58 Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Sat, 11 Apr 2026 23:17:39 +0200 Subject: [PATCH 03/20] Feat: Add start_Game command to server --- .../dbis/cs108/casono/server/ServerApp.java | 14 +++ .../lobby/start_game/StartGameHandler.java | 113 ++++++++++++++++++ .../lobby/start_game/StartGameParser.java | 27 +++++ .../lobby/start_game/StartGameRequest.java | 35 ++++++ .../lobby/start_game/StartGameResponse.java | 34 ++++++ 5 files changed, 223 insertions(+) create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/start_game/StartGameHandler.java create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/start_game/StartGameParser.java create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/start_game/StartGameRequest.java create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/start_game/StartGameResponse.java diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java index f9007eb..cec1b2a 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java @@ -197,5 +197,19 @@ public class ServerApp { .JoinLobbyRequest>) new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby .JoinLobbyHandler(responseDispatcher, lobbyManager, userRegistry)); + + // START_GAME registration + parserDispatcher.register( + "START_GAME", + new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game + .StartGameParser()); + commandRouter.register( + ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game + .StartGameRequest.class, + (ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler< + ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game + .StartGameRequest>) + new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game + .StartGameHandler(responseDispatcher, lobbyManager, userRegistry)); } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/start_game/StartGameHandler.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/start_game/StartGameHandler.java new file mode 100644 index 0000000..eb1f335 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/start_game/StartGameHandler.java @@ -0,0 +1,113 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game; + +import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.UserLoggedInCheck; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.GameEngine; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.RoundManager; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.TurnManager; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.rules.RuleEngine; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry; +import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher; +import java.util.ArrayList; +import java.util.Optional; + +/** + * Handler for START_GAME: creates a GameEngine+GameController for the lobby and starts the game. + */ +public class StartGameHandler extends CommandHandler { + private final LobbyManager lobbyManager; + private final UserRegistry userRegistry; + private static final int DEFAULT_START_CHIPS = 20000; + + public StartGameHandler( + ResponseDispatcher responseDispatcher, + LobbyManager lobbyManager, + UserRegistry userRegistry) { + super(responseDispatcher); + this.lobbyManager = lobbyManager; + this.userRegistry = userRegistry; + addCheck(new UserLoggedInCheck(userRegistry)); + } + + @Override + public void execute(StartGameRequest request) { + Optional opt = + userRegistry.getBySessionId(request.getSessionId()); + if (opt.isEmpty()) { + // Guard: UserLoggedInCheck should normally handle this + responseDispatcher.dispatch( + new ErrorResponse( + request.getContext(), "USER_NOT_LOGGED_IN", "User not logged in")); + return; + } + + String username = opt.get().getName(); + + // Resolve lobby by provided ID (required) + LobbyId lid = LobbyId.of(request.getId()); + Lobby lobby = lobbyManager.getLobby(lid); + if (lobby == null) { + responseDispatcher.dispatch( + new ErrorResponse(request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found")); + return; + } + + // Ensure requester is part of the lobby + if (!lobby.getPlayerNames().contains(username)) { + responseDispatcher.dispatch( + new ErrorResponse( + request.getContext(), "NOT_IN_LOBBY", "User not in specified lobby")); + return; + } + + // Prevent double-start + if (lobby.getGameController() != null) { + responseDispatcher.dispatch( + new ErrorResponse( + request.getContext(), + "ALREADY_STARTED", + "Game has already been started for this lobby")); + return; + } + + // Basic player count check + if (lobby.getPlayerNames().size() < 2) { + responseDispatcher.dispatch( + new ErrorResponse( + request.getContext(), + "NOT_ENOUGH_PLAYERS", + "Not enough players to start the game")); + return; + } + + // Create engine + controller + GameState state = new GameState(); + GameEngine engine = + new GameEngine( + state, + new RuleEngine(new ArrayList<>()), + new RoundManager(), + new TurnManager()); + GameController game = new GameController(engine); + + // Add all players from lobby + for (String playerName : lobby.getPlayerNames()) { + game.addPlayer(PlayerId.of(playerName), DEFAULT_START_CHIPS); + } + + // Start the game and attach to lobby + game.startGame(); + lobby.initGame(game); + + responseDispatcher.dispatch( + new StartGameResponse( + request.getContext(), lid.value(), "Game started successfully")); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/start_game/StartGameParser.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/start_game/StartGameParser.java new file mode 100644 index 0000000..807dfd8 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/start_game/StartGameParser.java @@ -0,0 +1,27 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor; + +/** + * Parser for the `START_GAME` command. + * + *

Expected request parameters: + * + *

    + *
  • `ID` (int) — numeric lobby identifier (required) + *
+ * + *

The parser builds a {@link StartGameRequest} containing the parsed lobby id and the original + * request context. + */ +public class StartGameParser implements CommandParser { + @Override + public StartGameRequest parse(PrimitiveRequest primitiveRequest) { + RequestParameterAccessor accessor = + new RequestParameterAccessor(primitiveRequest.parameters()); + int id = accessor.require("ID", Integer::parseInt); + return new StartGameRequest(primitiveRequest.context(), id); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/start_game/StartGameRequest.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/start_game/StartGameRequest.java new file mode 100644 index 0000000..616b6f9 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/start_game/StartGameRequest.java @@ -0,0 +1,35 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext; + +/** + * Request to start a game in a specific lobby. + * + *

The `START_GAME` request requires the numeric lobby id parameter `ID` identifying the target + * lobby. The request carries the usual {@link RequestContext} (session id, request id) via the base + * class. + */ +public class StartGameRequest extends Request { + private final int id; + + /** + * Create a new {@link StartGameRequest}. + * + * @param context the request context + * @param id numeric lobby id to start the game in + */ + public StartGameRequest(RequestContext context, int id) { + super(context); + this.id = id; + } + + /** + * Returns the numeric lobby id provided by the client. + * + * @return lobby id + */ + public int getId() { + return id; + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/start_game/StartGameResponse.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/start_game/StartGameResponse.java new file mode 100644 index 0000000..35ae0c0 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/start_game/StartGameResponse.java @@ -0,0 +1,34 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody; + +/** + * Success response for {@code START_GAME} containing the started game's identifier and an + * informational message. + */ +public class StartGameResponse extends SuccessResponse { + /** + * Create a standard start-game response with a default success message. + * + * @param context the request context + * @param gameId numeric id of the started game (uses lobby id) + */ + public StartGameResponse(RequestContext context, int gameId) { + this(context, gameId, "Game started successfully"); + } + + /** + * Create a start-game response with a custom message. + * + * @param context the request context + * @param gameId numeric id of the started game (uses lobby id) + * @param message informational message for the client + */ + public StartGameResponse(RequestContext context, int gameId, String message) { + super( + context, + ResponseBody.builder().param("GAME", gameId).param("MESSAGE", message).build()); + } +} From 371e6afe5ce2a70e482cd35e0509223a684e2580 Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Sat, 11 Apr 2026 23:18:22 +0200 Subject: [PATCH 04/20] Docs: add documentation of new start_game command --- .../networking/commands/protocol-document.md | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/documents/docs/networking/commands/protocol-document.md b/documents/docs/networking/commands/protocol-document.md index 8ac0631..378940a 100644 --- a/documents/docs/networking/commands/protocol-document.md +++ b/documents/docs/networking/commands/protocol-document.md @@ -78,6 +78,7 @@ This document describes the protocol for client-server communication in our appl - [Required pre-execution checks](#required-pre-execution-checks) - [Request Parameters](#request-parameters) - [Success Response](#success-response) + - [START_GAME command](#start_game-command) - [GET_LOBBY_STATUS command](#get_lobby_status-command) - [Required pre-execution checks](#required-pre-execution-checks) - [Request Parameters](#request-parameters) @@ -543,8 +544,64 @@ END ``` -ERR CODE=LOBBY_NOT_FOUND - MESSAGE=Lobby not found + MSG=Lobby not found END + +## START_GAME command + +The `START_GAME` command requests the server to start a new game in the specified lobby. The +server requires an explicit numeric `ID` parameter identifying the target lobby. + +### Required pre-execution checks +- [`UserLoggedInCheck`](#userloggedincheck) + +### Request Parameters +| Parameter Name | Type | Optional | Description | +| :------------- | :--- | :------: | :---------- | +| `ID` | `int` | no | Numeric id of the target lobby | + +### Implementation notes + +- Parser: `StartGameParser` — reads the required `ID` parameter and builds `StartGameRequest`. +- Handler: `StartGameHandler` — resolves the lobby by id, checks the requester is a member of the + lobby, verifies a game is not already running and that enough players are present, initializes + `GameController` and attaches it to the lobby. On success a structured `START_GAME` success + response containing `GAME` and `MESSAGE` fields is returned. + +### Success Response +| Field | Type | Description | +| :------- | :------ | :-------------------------------- | +| `GAME` | `int` | Numeric id of the started game (uses lobby id) | +| `MESSAGE`| `String`| Human-readable status message | + +### Error Response +| Code | Description | +| :--------------- | :---------------------------------------- | +| `MISSING_PARAMETER` | Required parameter `ID` missing (parser) | +| `LOBBY_NOT_FOUND` | The specified lobby id does not exist | +| `NOT_IN_LOBBY` | Requesting user is not a member of the lobby | +| `NOT_ENOUGH_PLAYERS` | Not enough players to start the game | +| `ALREADY_STARTED` | A game is already running in the lobby | +| `USER_NOT_LOGGED_IN` | No user associated with the session | + +### Example Request + +``` + +START_GAME ID=1 + +``` + +### Example Response (success) + +``` + ++OK + GAME=1 + MESSAGE=Game started successfully +END + +``` ``` ## GET_LOBBY_LIST command From b2adeb7a5604439133b23c86d18ab4519c64d4b0 Mon Sep 17 00:00:00 2001 From: Mathis Ginkel Date: Sun, 12 Apr 2026 00:19:34 +0200 Subject: [PATCH 05/20] Feat: Add whisper chat functionality Introduce recording of users connected to server, to show usernames as options for whisper chat. --- .../casono/client/chat/ChatController.java | 53 +++++++-- .../casono/client/network/ChatClient.java | 28 ++++- .../client/ui/chatui/ChatBoxController.java | 108 +++++++++++++----- .../dbis/cs108/casono/client/chat/Chat.java | 2 +- .../casono/client/chat/ChatApplication.java | 18 ++- 5 files changed, 159 insertions(+), 50 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatController.java index 161397a..5c8b50a 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatController.java @@ -3,11 +3,12 @@ package ch.unibas.dmi.dbis.cs108.casono.client.chat; import ch.unibas.dmi.dbis.cs108.casono.client.network.ChatClient; import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService; import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Timer; -import java.util.TimerTask; + +import java.util.*; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.jspecify.annotations.Nullable; /** @@ -38,10 +39,14 @@ public class ChatController { return chatModelMap; } - private Map chatModelMap; + private final Map chatModelMap; private static final long REFRESH_TIME = 1000; + private List localUserList; + + private Logger logger; + /** * Constructor, adds TimerTask to be sent to the server regularly * @@ -52,13 +57,16 @@ public class ChatController { this.username = username; chatClient = new ChatClient(clientService); chatModelMap = new LinkedHashMap<>(); + localUserList = new ArrayList<>(); this.chatBoxController = new ChatBoxController(username, this); + this.logger = LogManager.getLogger(ChatController.class); this.timer = new Timer(); timer.schedule( new TimerTask() { @Override public void run() { receiveMessage(); + checkWhisperUsers(); } }, 0, @@ -102,14 +110,21 @@ public class ChatController { } break; case ChatType.WHISPER: - if (msg.target.equals(username)) { - if (chatModelMap.containsKey( - new ChatKey(ChatType.WHISPER, msg.sender))) { + if (msg.target.equals(username) || msg.sender.equals(username)) { + ChatKey key; + if(msg.target.equals(username)) { + key = new ChatKey(ChatType.WHISPER, msg.sender); + } else { + key = new ChatKey(ChatType.WHISPER, msg.target); + } + if (chatModelMap.containsKey(key)) { chatModelMap - .get(new ChatKey(ChatType.WHISPER, msg.sender)) + .get(key) .addMessage(msg); } else { - chatBoxController.addWhisperChat(msg.sender); + ChatModel chatModel = new ChatModel(ChatType.WHISPER, username, lobbyId, key.targetUser()); + chatBoxController.addWhisperChat(key.targetUser(), chatModel); + chatModel.addMessage(msg); } } break; @@ -118,6 +133,22 @@ public class ChatController { } } + public synchronized void checkWhisperUsers() { + List users = chatClient.getUsers(); + logger.info(users); + if (!users.isEmpty()) { + for (String user : users) { + String value = user.split("\\=")[1]; + logger.info(value); + if (!(localUserList.contains(value) || value.equals(username))) { + localUserList.add(value); + logger.info("adding new whisper user"); + chatBoxController.addWhisperUser(value); + } + } + } + } + /** * method to send a message to the server * diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ChatClient.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ChatClient.java index f47ea87..e6fe86e 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ChatClient.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ChatClient.java @@ -2,8 +2,9 @@ package ch.unibas.dmi.dbis.cs108.casono.client.network; import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message; import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter; -import java.util.ArrayList; -import java.util.List; + +import java.util.*; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -69,4 +70,27 @@ public class ChatClient { } return messages; } + + public List getUsers() { + logger.info("Asking server for list of users"); + List users = clientService.processCommand("LIST_USERS"); + List parameters = new ArrayList(); + for (int i = 0; i < users.size(); i++) { + String line = users.get(i); + if (line.equals("USERS")) { + continue; + } else if (line.equals("END")) { + break; + } + line = line.replaceFirst("^\t", ""); + + if (line.equals("USER")) { + String username = users.get(i + 1); + username = username.replaceFirst("^\t", ""); + username = username.replaceFirst("^\t", ""); + parameters.add(username); + } + } + return parameters; + } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatBoxController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatBoxController.java index 618d309..3e5c518 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatBoxController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatBoxController.java @@ -5,14 +5,20 @@ import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatModel; import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatType; import java.io.IOException; import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import javafx.application.Platform; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.Node; -import javafx.scene.control.MenuButton; -import javafx.scene.control.MenuItem; -import javafx.scene.control.Tab; -import javafx.scene.control.TabPane; +import javafx.scene.control.*; +import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; @@ -28,15 +34,21 @@ public class ChatBoxController { @FXML private MenuButton addWhisperChatButton; + @FXML private HBox menuBox; + private FXMLLoader fxmlLoader; - @FXML private List whisperUsers; + private final List activeWhisperChats; + + @FXML private final Map usernameTabMap; private String ressource = "/ui-structure/components/chatui/chattab.fxml"; public ChatBoxController(String username, ChatController chatController) { this.username = username; this.chatController = chatController; + activeWhisperChats = new ArrayList<>(); + usernameTabMap = new HashMap<>(); } /** @@ -51,7 +63,7 @@ public class ChatBoxController { .getChatModelMap() .put(new ChatController.ChatKey(ChatType.GLOBAL), globalChatModel); addChatTab("GLOBAL", globalChatModel); - // TODO: Button to add new Whisper Chat + addWhisperChatButton.setOnAction(event -> addWhisperChatButton.show()); } /** @@ -61,10 +73,12 @@ public class ChatBoxController { * @param targetUserName The username of the person to be added to the whisper list. */ public void addWhisperUser(String targetUserName) { - MenuItem menuItem = new MenuItem(targetUserName); - whisperUsers.add(menuItem); - addWhisperChatButton.getItems().add(menuItem); - menuItem.setOnAction(event -> addWhisperChat(targetUserName)); + Platform.runLater( + () -> { + MenuItem menuItem = new MenuItem(targetUserName); + addWhisperChatButton.getItems().add(menuItem); + menuItem.setOnAction(event -> addWhisperChat(targetUserName, new ChatModel(ChatType.WHISPER, username, -1, targetUserName))); + }); } /** @@ -73,12 +87,16 @@ public class ChatBoxController { * * @param target The username of the recipient for the private messages. */ - public void addWhisperChat(String target) { - ChatModel chatModel = new ChatModel(ChatType.WHISPER, username, -1, target); - chatController - .getChatModelMap() - .put(new ChatController.ChatKey(ChatType.WHISPER, target), chatModel); - addChatTab(target, chatModel); + public void addWhisperChat(String target, ChatModel chatModel) { + if (!activeWhisperChats.contains(target)) { + activeWhisperChats.add(target); + chatController + .getChatModelMap() + .put(new ChatController.ChatKey(ChatType.WHISPER, target), chatModel); + addChatTab(target, chatModel); + } else { + chatTabPane.getSelectionModel().select(usernameTabMap.get(target)); + } } /** @@ -93,20 +111,50 @@ public class ChatBoxController { public void addChatTab(String title, ChatModel chatModel) { URL resource = getClass().getResource(ressource); FXMLLoader fxmlLoader = new FXMLLoader(resource); - try { - ChatViewController chatViewController = - new ChatViewController(this.chatController, chatModel, this.username); - fxmlLoader.setController(chatViewController); - Node load = fxmlLoader.load(); - VBox.setVgrow(load, Priority.ALWAYS); - VBox vbox = new VBox(); - VBox.setVgrow(vbox, Priority.ALWAYS); - vbox.getChildren().add(load); - chatModel.addListener((msg) -> chatViewController.showMessage(msg)); - Tab newChat = new Tab(title, vbox); - this.chatTabPane.getTabs().add(newChat); - } catch (IOException e) { - throw new RuntimeException(e); + runOnPlatformSynchronized(()-> { + try { + ChatViewController chatViewController = + new ChatViewController(this.chatController, chatModel, this.username); + fxmlLoader.setController(chatViewController); + Node load = fxmlLoader.load(); + VBox.setVgrow(load, Priority.ALWAYS); + VBox vbox = new VBox(); + VBox.setVgrow(vbox, Priority.ALWAYS); + vbox.getChildren().add(load); + chatModel.addListener((msg) -> chatViewController.showMessage(msg)); + Tab newChat = new Tab(title, vbox); + usernameTabMap.put(title, newChat); + this.chatTabPane.getTabs().add(newChat); + this.chatTabPane.getSelectionModel().select(newChat); + return newChat; + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + public static T runOnPlatformSynchronized(Callable code) { + if(Platform.isFxApplicationThread()) { + try { + return code.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + CompletableFuture futureLock = new CompletableFuture<>(); + Platform.runLater(() -> { + try { + futureLock.complete(code.call()); + } catch (Exception e) { + futureLock.completeExceptionally(e); + } + }); + try { + return futureLock.get(); + } catch (Throwable e) { + throw new RuntimeException(e); + } } } + } diff --git a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/Chat.java b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/Chat.java index 6088f22..5f533fd 100644 --- a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/Chat.java +++ b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/Chat.java @@ -4,6 +4,6 @@ import javafx.application.Application; public class Chat { public static void main(String[] args) { - Application.launch(ChatApplication.class); + Application.launch(ChatApplication.class, args); } } diff --git a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatApplication.java b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatApplication.java index 0eff249..722b859 100644 --- a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatApplication.java +++ b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatApplication.java @@ -4,8 +4,13 @@ import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService; import ch.unibas.dmi.dbis.cs108.casono.client.network.CoreClient; import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + import javafx.application.Application; import javafx.fxml.FXMLLoader; +import javafx.scene.Node; +import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; @@ -13,13 +18,14 @@ public class ChatApplication extends Application { private static final int SCENE_WIDTH = 1200; private static final int SCENE_HEIGHT = 800; - String ip = "localhost"; - String username = "mathis"; - final int port = 5000; public ChatApplication() {} public void start(Stage stage) throws IOException { + List params = getParameters().getRaw(); + String ip = params.get(0); + int port = Integer.parseInt(params.get(1)); + String username = params.get(2); FXMLLoader fxmlLoader = new FXMLLoader( getClass().getResource("/ui-structure/components/chatui/chatbox.fxml")); @@ -27,10 +33,10 @@ public class ChatApplication extends Application { CoreClient coreClient = new CoreClient(clientService); coreClient.login(username); ChatController chatController = new ChatController(username, clientService); - ChatBoxController chatBoxController = new ChatBoxController(username, chatController); + ChatBoxController chatBoxController = chatController.getChatBoxController(); fxmlLoader.setController(chatBoxController); - - Scene scene = new Scene(fxmlLoader.load(), SCENE_WIDTH, SCENE_HEIGHT); + Parent node = fxmlLoader.load(); + Scene scene = new Scene(node, SCENE_WIDTH, SCENE_HEIGHT); stage.setTitle("Chat"); stage.setScene(scene); stage.show(); From 869f061d86ffd3bbb7ae5f93f23998a89a4c7149 Mon Sep 17 00:00:00 2001 From: Julian Kropff Date: Sun, 12 Apr 2026 10:58:16 +0200 Subject: [PATCH 06/20] Fix: update GameClient to support server commands Refs #52 --- .../casono/client/network/GameClient.java | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java index 685e870..e8e3cae 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java @@ -34,30 +34,37 @@ public class GameClient { * @return A GameState object representing the current state of the game, as parsed */ public GameState fetchGameState() { - List responseLines = client.processCommand("GET_GAME_STATE\nGAME_ID=" + gameId); + List responseLines = client.processCommand("GET_GAME_STATE GAME_ID=" + gameId); if (responseLines == null || responseLines.isEmpty()) { throw new RuntimeException("Empty server response"); } - String fullResponse = String.join("\n", responseLines); + if (responseLines.get(0).startsWith("-ERR")) { + throw new RuntimeException("Server Error: " + String.join(" ", responseLines)); + } + if (!responseLines.get(0).startsWith("+OK")) { + throw new RuntimeException("Invalid response: missing +OK"); + } + + String fullResponse = String.join("\n", responseLines); return parse(fullResponse); } /** Send a CALL command to the server to indicate that the player wants to call */ public void sendCall() { - client.processCommand("CALL\nGAME_ID=" + gameId); + client.processCommand("CALL GAME_ID=" + gameId); } /** Send a FOLD command to the server to indicate that the player wants to fold */ public void sendFold() { - client.processCommand("FOLD\nGAME_ID=" + gameId); + client.processCommand("FOLD GAME_ID=" + gameId); } /** Send a BET command to the server to indicate that the player wants to bet */ public void sendBet(int amount) { - client.processCommand("BET\nGAME_ID=" + gameId + "\nAMOUNT=" + amount); + client.processCommand("BET GAME_ID=" + gameId + " AMOUNT=" + amount); } /** @@ -66,7 +73,7 @@ public class GameClient { * @param amount The amount to raise to */ public void sendRaise(int amount) { - client.processCommand("RAISE\nGAME_ID=" + gameId + "\nAMOUNT=" + amount); + client.processCommand("RAISE GAME_ID=" + gameId + " AMOUNT=" + amount); } /** @@ -76,8 +83,12 @@ public class GameClient { * @return A GameState object representing the current state of the game. */ private GameState parse(String input) { - GameState state = new GameState(); + if (input.startsWith("-ERR")) { + throw new RuntimeException("Server returned Error:\n" + input); + } + + GameState state = new GameState(); ParserContext ctx = new ParserContext(state); for (String raw : input.split("\n")) { @@ -156,6 +167,7 @@ public class GameClient { state.winnerIndex = intVal(line); } else { return false; + // throw new RuntimeException("Unknown global field: " + line); } return true; } @@ -171,7 +183,7 @@ public class GameClient { private boolean handleSections(String line, ParserContext ctx) { if (line.equals("END")) { - ctx.inPlayers = false; + // ctx.inPlayers = false; ctx.inPlayerCards = false; ctx.inCommunityCards = false; ctx.currentPlayer = null; From 8f3d0ab8a983385ce3ed62246962cda5f397ef92 Mon Sep 17 00:00:00 2001 From: Julian Kropff Date: Sun, 12 Apr 2026 10:58:54 +0200 Subject: [PATCH 07/20] Fix: update GameService to support server commands Refs #52 --- .../dbis/cs108/casono/client/game/GameService.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/game/GameService.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/game/GameService.java index e58c13d..896e8bf 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/game/GameService.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/game/GameService.java @@ -39,6 +39,7 @@ public class GameService { * @return The current GameState object representing the state of the game. */ public int getPot() { + ensureState(); return state.pot; } @@ -48,6 +49,7 @@ public class GameService { * @return The current GameState object representing the state of the game. */ public List getCommunityCards() { + ensureState(); return state.communityCards; } @@ -57,6 +59,7 @@ public class GameService { * @return A list of Player objects representing the players in the current game state. */ public List getPlayers() { + ensureState(); return state.players; } @@ -98,10 +101,19 @@ public class GameService { * yet. */ public Player getWinner() { + ensureState(); + if (state.winnerIndex < 0) { return null; } return state.players.get(state.winnerIndex); } + + /** Ensure that the game state has been initialized before accessing it. */ + private void ensureState() { + if (state == null) { + throw new IllegalStateException("Call refresh() first"); + } + } } From 7316ec80bfd50977062dede4608ca0d1be72df40 Mon Sep 17 00:00:00 2001 From: Julian Kropff Date: Sun, 12 Apr 2026 11:42:20 +0200 Subject: [PATCH 08/20] Fix: cleanup CasinoGameController and fix lifecycle issues Refs #52 --- .../ui/gameui/CasinoGameController.java | 91 +++++++++++++------ 1 file changed, 64 insertions(+), 27 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java index 05abedb..226b5f1 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java @@ -48,6 +48,7 @@ public class CasinoGameController { private PlayerId myPlayerId; private TaskbarController taskbarController; private Image dealerImage; + private javafx.animation.Timeline timeline; private static final int TOTAL_SLOTS = 5; private static final int PLAYER_SLOTS = 2; @@ -166,6 +167,8 @@ public class CasinoGameController { // uiTest(); + // or + // empty display only (optional) renderCommunityCards(List.of()); renderPlayerCards(List.of()); @@ -277,42 +280,69 @@ public class CasinoGameController { */ private void startLoop() { - javafx.animation.Timeline t = + timeline = new javafx.animation.Timeline( new javafx.animation.KeyFrame( javafx.util.Duration.seconds(UI_UPDATE_INTERVAL_SECONDS), e -> updateUI())); - t.setCycleCount(javafx.animation.Animation.INDEFINITE); - t.play(); + timeline.setCycleCount(javafx.animation.Animation.INDEFINITE); + timeline.play(); } /** - * Update the UI by fetching the latest game state from the GameService and updating all - * relevant components. + * Stop the UI update loop, which can be called when the game ends or when the user exits the + * game screen to prevent unnecessary updates and resource usage. + */ + public void stop() { + if (timeline != null) { + timeline.stop(); + } + } + + /** + * Triggers an asynchronous UI update by fetching the current game state from the GameService. */ private void updateUI() { - try { - GameState s = gameService.refresh(); + java.util.concurrent.CompletableFuture.supplyAsync( + () -> { + try { + return gameService.refresh(); + } catch (Exception e) { + LOGGER.severe("GameService Error: " + e.getMessage()); + return null; + } + }) + .thenAccept( + s -> { + if (s == null) { + return; + } - if (s == null) { - return; - } + javafx.application.Platform.runLater(() -> applyState(s)); + }); + } - renderCommunityCards(s.communityCards); - renderPot(s.pot); + /** + * Updates all visual UI components with the data from the provided GameState. This includes + * community cards, pot, player information, and the taskbar. + * + * @param s The current state of the game to be rendered. + */ + private void applyState(GameState s) { - updatePlayers(s.players); - updatePlayerCards(s); + renderCommunityCards(s.communityCards); + renderPot(s.pot); - updateGameInfo(s); - highlightDealer(s); + updatePlayers(s.players); + updatePlayerCards(s); - updateTaskbar(s); + updateGameInfo(s); + highlightDealer(s); - } catch (Exception e) { - LOGGER.severe("Error: UI Update failed: " + e.getMessage()); + if (taskbarController != null) { + taskbarController.update(s, myPlayerId); } } @@ -323,14 +353,20 @@ public class CasinoGameController { */ private void updatePlayerCards(GameState s) { - int active = s.activePlayer; - - if (active >= 0 && active < s.players.size()) { - Player p = s.players.get(active); - renderPlayerCards(p.getCards()); - } else { + if (s.players == null || myPlayerId == null) { renderPlayerCards(List.of()); + return; } + + for (Player p : s.players) { + if (p.getId().equals(myPlayerId)) { + renderPlayerCards(p.getCards()); + return; + } + } + + // fallback + renderPlayerCards(List.of()); } /** @@ -357,8 +393,9 @@ public class CasinoGameController { * @param s The current game state. */ private void updateTaskbar(GameState s) { - - taskbarController.update(s, myPlayerId); + if (taskbarController != null) { + taskbarController.update(s, myPlayerId); + } } /** From e6c222388b9fce54934f85341a2231bea5f49b30 Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Sun, 12 Apr 2026 12:03:07 +0200 Subject: [PATCH 09/20] Fix: add correct parsing on client side for create_lobyb command --- .../casono/client/network/LobbyClient.java | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/LobbyClient.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/LobbyClient.java index 6d43f19..9f6f4a8 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/LobbyClient.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/LobbyClient.java @@ -41,8 +41,25 @@ public class LobbyClient { * @return The id of the newly created lobby, as returned by the server. */ public int createLobby() { - String response = client.processCommand("CREATE_LOBBY").getFirst(); - return Integer.parseInt(response); + List lines = client.processCommand("CREATE_LOBBY"); + + List params = ClientService.convertToRequestParameters(lines); + for (RequestParameter p : params) { + if ("LOBBY_ID".equalsIgnoreCase(p.key())) { + return Integer.parseInt(p.value()); + } + } + + // Fallback for simple legacy/test servers that return the id as a single plain + // line + if (!lines.isEmpty()) { + try { + return Integer.parseInt(lines.get(0)); + } catch (NumberFormatException ignored) { + } + } + + throw new RuntimeException("No LOBBY_ID in response: " + lines); } /** From fa8bb25b0387f879bc04f34bc411c0f5ffae42ad Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Sun, 12 Apr 2026 12:03:41 +0200 Subject: [PATCH 10/20] Add: Register create_lobby command --- .../dmi/dbis/cs108/casono/server/ServerApp.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java index 5dedaf3..b693217 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java @@ -239,6 +239,20 @@ public class ServerApp { .get_lobby_status.GetLobbyStatusHandler( responseDispatcher, lobbyManager, userRegistry)); + // CREATE_LOBBY registration + parserDispatcher.register( + "CREATE_LOBBY", + new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby + .CreateLobbyParser()); + commandRouter.register( + ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby + .CreateLobbyRequest.class, + (ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler< + ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby + .create_lobby.CreateLobbyRequest>) + new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby + .CreateLobbyHandler(responseDispatcher, lobbyManager)); + // JOIN_LOBBY registration parserDispatcher.register( "JOIN_LOBBY", From 659ea8a744d1a4509705939b32e78b3db9ef7e64 Mon Sep 17 00:00:00 2001 From: Julian Kropff Date: Sun, 12 Apr 2026 12:53:44 +0200 Subject: [PATCH 11/20] Fix: update Lobby Button styles in Casino Main UI --- .../resources/ui-structure/Casinomainui.css | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/main/resources/ui-structure/Casinomainui.css b/src/main/resources/ui-structure/Casinomainui.css index 72765de..deb3d38 100644 --- a/src/main/resources/ui-structure/Casinomainui.css +++ b/src/main/resources/ui-structure/Casinomainui.css @@ -6,10 +6,10 @@ .anchor-pane-bg { -fx-background-color: #ffffff; /* Optional: Remove the image or use it as an overlay */ - -fx-background-image: url("/images/background.png"); - -fx-background-size: cover; - -fx-background-position: center center; - -fx-background-repeat: no-repeat; + -fx-background-image: url("/images/background.png"); + -fx-background-size: cover; + -fx-background-position: center center; + -fx-background-repeat: no-repeat; } /* THE FLOATING INFO BOX */ @@ -244,3 +244,15 @@ -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3); -fx-background-color: #27ae60; } + +#lobbyBtn-1, +#lobbyBtn-2, +#lobbyBtn-3, +#lobbyBtn-4, +#lobbyBtn-5, +#lobbyBtn-6, +#lobbyBtn-7, +#lobbyBtn-8 { + -fx-background-color: transparent; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3); +} From 1dcf628d2c26f63ce888fb0f6ed7dbdabef3648d Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Sun, 12 Apr 2026 13:08:57 +0200 Subject: [PATCH 12/20] Feat: Log player joins in LobbyManager --- .../cs108/casono/server/domain/lobby/LobbyManager.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyManager.java index 2179e8a..515aee4 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyManager.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyManager.java @@ -69,6 +69,15 @@ public class LobbyManager { lobby.addPlayerAndMaybeStart( username, maxPlayersPerLobby, AUTO_START_PLAYERS, DEFAULT_START_CHIPS); if (result != AddResult.NOT_ADDED) { + LOGGER.info( + () -> + "User '" + + username + + "' joined lobby " + + lobbyId.value() + + " (result=" + + result + + ")"); playerToLobby.put(username, lobbyId); if (result == AddResult.ADDED_AND_STARTED) { LOGGER.info( From e43732c52053002872d251abb2aa68005b9d519b Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Sun, 12 Apr 2026 13:12:55 +0200 Subject: [PATCH 13/20] Feat: Log JOIN_LOBBY requests and joins --- .../lobby/join_lobby/JoinLobbyHandler.java | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/join_lobby/JoinLobbyHandler.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/join_lobby/JoinLobbyHandler.java index 579c281..169b027 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/join_lobby/JoinLobbyHandler.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/join_lobby/JoinLobbyHandler.java @@ -9,6 +9,8 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandH import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; /** * Handler for the `JOIN_LOBBY` command. @@ -21,6 +23,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatch public class JoinLobbyHandler extends CommandHandler { private final LobbyManager lobbyManager; private final UserRegistry userRegistry; + private static final Logger LOGGER = LogManager.getLogger(JoinLobbyHandler.class); /** * Create a new {@link JoinLobbyHandler}. @@ -46,9 +49,17 @@ public class JoinLobbyHandler extends CommandHandler { */ @Override public void execute(JoinLobbyRequest request) { + LOGGER.info( + "JOIN_LOBBY request: session={}, lobbyId={}", + request.getContext().sessionId(), + request.getId()); LobbyId lid = LobbyId.of(request.getId()); var lobby = lobbyManager.getLobby(lid); if (lobby == null) { + LOGGER.warn( + "JOIN_LOBBY: Lobby {} not found (session={})", + request.getId(), + request.getContext().sessionId()); responseDispatcher.dispatch( new ErrorResponse(request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found")); return; @@ -56,7 +67,9 @@ public class JoinLobbyHandler extends CommandHandler { var maybeUser = userRegistry.getBySessionId(request.getContext().sessionId()); if (maybeUser.isEmpty()) { - // UserLoggedInCheck should normally prevent this; keep safe fallback + LOGGER.warn( + "JOIN_LOBBY: No user associated with session {}", + request.getContext().sessionId()); responseDispatcher.dispatch( new ErrorResponse( request.getContext(), @@ -70,6 +83,10 @@ public class JoinLobbyHandler extends CommandHandler { boolean ok = lobbyManager.addPlayerToLobby(username, lid); if (!ok) { + LOGGER.warn( + "JOIN_LOBBY: User '{}' failed to join lobby {} (full or already in)", + username, + request.getId()); responseDispatcher.dispatch( new ErrorResponse( request.getContext(), @@ -78,6 +95,8 @@ public class JoinLobbyHandler extends CommandHandler { return; } + LOGGER.info("User '{}' joined lobby {}", username, request.getId()); + responseDispatcher.dispatch(new OkResponse(request.getContext())); } } From 3abc03e6e1c36f9e994b9cf206a87afc4cecff75 Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Sun, 12 Apr 2026 13:25:59 +0200 Subject: [PATCH 14/20] Fix: fix error when switching back to mainLobby when exiting gameUI --- .../gameuicomponents/TaskbarController.java | 124 ++++++++++++------ 1 file changed, 84 insertions(+), 40 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java index 667a584..431889f 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java @@ -5,7 +5,7 @@ import ch.unibas.dmi.dbis.cs108.casono.client.game.GameState; import ch.unibas.dmi.dbis.cs108.casono.client.game.Player; import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId; import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerState; -import ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui; +// Casinomainui is loaded via FXMLLoader when switching back to the lobby UI import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.Label; @@ -20,20 +20,29 @@ import org.apache.logging.log4j.Logger; /** * Controller for the interactive taskbar within the poker UI. * - *

Responsible for: - Drag-and-drop movement of the taskbar, - Input and management of game + *

+ * Responsible for: - Drag-and-drop movement of the taskbar, - Input and + * management of game * stakes, - Control of general menu functions such as Exit. */ public class TaskbarController { private static final Logger LOGGER = LogManager.getLogger(CasinoBrowserController.class); - @FXML private HBox taskbar; - @FXML private TextField taskbarInput; - @FXML private Button betButton; - @FXML private Button callButton; - @FXML private Button foldButton; - @FXML private Button raiseButton; - @FXML private Label moneyLabel; + @FXML + private HBox taskbar; + @FXML + private TextField taskbarInput; + @FXML + private Button betButton; + @FXML + private Button callButton; + @FXML + private Button foldButton; + @FXML + private Button raiseButton; + @FXML + private Label moneyLabel; private GameService gameService; private PlayerId myPlayerId; @@ -57,10 +66,12 @@ public class TaskbarController { } /** - * Updates the state of the basic action buttons (Bet, Call, Fold, Raise) and the input field + * Updates the state of the basic action buttons (Bet, Call, Fold, Raise) and + * the input field * * @param isMyTurn Indicates whether it is currently the player's turn. - * @param isOut Indicates whether the player is currently out of the game (folded or all-in). + * @param isOut Indicates whether the player is currently out of the game + * (folded or all-in). */ private void updateBasicButtons(boolean isMyTurn, boolean isOut) { @@ -89,7 +100,8 @@ public class TaskbarController { } /** - * Sets the given button to a disabled state with red styling, indicating that the player is out + * Sets the given button to a disabled state with red styling, indicating that + * the player is out * * @param b The button to be styled as red and disabled */ @@ -105,7 +117,8 @@ public class TaskbarController { } /** - * Sets the given input field to a disabled state with red styling, indicating that the player + * Sets the given input field to a disabled state with red styling, indicating + * that the player * is out * * @param t The text field to be styled as red and disabled @@ -122,7 +135,8 @@ public class TaskbarController { } /** - * Activates the given button by enabling it and applying yellow styling, indicating that it is + * Activates the given button by enabling it and applying yellow styling, + * indicating that it is * the player's turn * * @param b The button to be activated and styled for the player's turn @@ -138,7 +152,8 @@ public class TaskbarController { } /** - * Activates the given input field by enabling it and applying yellow styling, indicating that + * Activates the given input field by enabling it and applying yellow styling, + * indicating that * it is the player's turn * * @param t The text field to be activated and styled for the player's turn @@ -154,7 +169,8 @@ public class TaskbarController { } /** - * Deactivates the given button by disabling it and applying gray styling, indicating that it is + * Deactivates the given button by disabling it and applying gray styling, + * indicating that it is * not the player's turn * * @param b The button to be deactivated and styled for non-active state @@ -171,7 +187,8 @@ public class TaskbarController { } /** - * Deactivates the given input field by disabling it and applying gray styling, indicating that + * Deactivates the given input field by disabling it and applying gray styling, + * indicating that * it is not the player's turn * * @param t The text field to be deactivated and styled for non-active state @@ -188,7 +205,8 @@ public class TaskbarController { } /** - * Updates the displayed amount of money the player has. The amount is shown in the format "X$". + * Updates the displayed amount of money the player has. The amount is shown in + * the format "X$". * * @param amount The amount of money to display for the player. */ @@ -202,7 +220,8 @@ public class TaskbarController { } /** - * Initializes the taskbar controller. This method is called automatically after the FXML + * Initializes the taskbar controller. This method is called automatically after + * the FXML * components have been loaded. */ @FXML @@ -211,8 +230,10 @@ public class TaskbarController { } /** - * Updates the taskbar based on the current game state and the player's status. It checks if - * it's the player's turn and whether they are out of the game (folded or all-in) to adjust the + * Updates the taskbar based on the current game state and the player's status. + * It checks if + * it's the player's turn and whether they are out of the game (folded or + * all-in) to adjust the * button states and displayed money accordingly. * * @param state @@ -225,11 +246,10 @@ public class TaskbarController { return; } - Player me = - state.players.stream() - .filter(p -> myPlayerId.equals(p.getId())) - .findFirst() - .orElse(null); + Player me = state.players.stream() + .filter(p -> myPlayerId.equals(p.getId())) + .findFirst() + .orElse(null); if (me == null) { LOGGER.error("Player not found in GameState!"); @@ -246,7 +266,8 @@ public class TaskbarController { } /** - * Called when the taskbar is clicked with the mouse. Saves the relative position for later, + * Called when the taskbar is clicked with the mouse. Saves the relative + * position for later, * correct repositioning. * * @param event The mouse event @@ -258,10 +279,13 @@ public class TaskbarController { } /** - * Called while dragging the taskbar with the mouse. Updates the position and slightly scales + * Called while dragging the taskbar with the mouse. Updates the position and + * slightly scales * the taskbar for visual feedback. * - *

TODO: It still needs to be fixed that the taskbar cannot disappear out of the window. + *

+ * TODO: It still needs to be fixed that the taskbar cannot disappear out of the + * window. * * @param event Das Mausereignis */ @@ -275,7 +299,8 @@ public class TaskbarController { } /** - * Called when the mouse cursor is released over the taskbar. Resets the taskbar scaling to + * Called when the mouse cursor is released over the taskbar. Resets the taskbar + * scaling to * normal size. * * @param event The mouse event @@ -299,7 +324,8 @@ public class TaskbarController { } /** - * Called when the submit button in the taskbar is pressed. Triggers the processing of the + * Called when the submit button in the taskbar is pressed. Triggers the + * processing of the * deployment. */ @FXML @@ -360,7 +386,8 @@ public class TaskbarController { } /** - * Refreshes the game state by fetching the latest state from the GameService and updating the + * Refreshes the game state by fetching the latest state from the GameService + * and updating the * taskbar accordingly. */ private void refreshGame() { @@ -375,28 +402,43 @@ public class TaskbarController { } /** - * Called when the Exit button is clicked. Closes the current game stage and opens the lobby UI. + * Called when the Exit button is clicked. Closes the current game stage and + * opens the lobby UI. */ @FXML private void onExitButtonClick() { javafx.application.Platform.runLater( () -> { // Close game stage - javafx.stage.Stage currentStage = - (javafx.stage.Stage) taskbar.getScene().getWindow(); + javafx.stage.Stage currentStage = (javafx.stage.Stage) taskbar.getScene().getWindow(); currentStage.close(); // Start lobby UI try { - new Casinomainui().start(new javafx.stage.Stage()); + javafx.stage.Stage newStage = new javafx.stage.Stage(); + javafx.fxml.FXMLLoader fxmlLoader = new javafx.fxml.FXMLLoader( + getClass().getResource("/ui-structure/Casinomainui.fxml")); + javafx.scene.Parent root = fxmlLoader.load(); + javafx.scene.Scene scene = new javafx.scene.Scene(root, 1200, 800); + newStage.setTitle("Casono"); + javafx.scene.image.Image icon = new javafx.scene.image.Image( + getClass() + .getResource("/images/logoinverted.png") + .toExternalForm()); + newStage.getIcons().add(icon); + newStage.setScene(scene); + newStage.setFullScreen(true); + newStage.show(); } catch (Exception e) { - LOGGER.error("Error: starting the lobby UI: {}", e.getMessage()); + LOGGER.error("Error: starting the lobby UI: {}", e.getMessage(), e); } }); } /** - * Processes the stake entered in the text field. Only integer values between 5 and 100,000 - * credits are accepted, in multiples of 5 (in increments of 5). The stake is currently only + * Processes the stake entered in the text field. Only integer values between 5 + * and 100,000 + * credits are accepted, in multiples of 5 (in increments of 5). The stake is + * currently only * displayed on the console. */ private void processBet() { @@ -434,7 +476,9 @@ public class TaskbarController { /** * Opens the integrated Casono web browser. * - *

TODO: Replace the start URL with the official project website (e.g., Tips & Tricks page) + *

+ * TODO: Replace the start URL with the official project website (e.g., Tips & + * Tricks page) * once the content for strategies and support is available. */ @FXML From 4fa60ad9e045d9eca794128e415bc31153c9029a Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Sun, 12 Apr 2026 13:30:11 +0200 Subject: [PATCH 15/20] Fix: Apply checkstyle fixes --- .../gameuicomponents/TaskbarController.java | 125 +++++++----------- .../client/ui/lobbyui/Casinomainui.java | 4 +- 2 files changed, 51 insertions(+), 78 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java index 431889f..096ac28 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java @@ -5,7 +5,7 @@ import ch.unibas.dmi.dbis.cs108.casono.client.game.GameState; import ch.unibas.dmi.dbis.cs108.casono.client.game.Player; import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId; import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerState; -// Casinomainui is loaded via FXMLLoader when switching back to the lobby UI +import ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.Label; @@ -20,29 +20,20 @@ import org.apache.logging.log4j.Logger; /** * Controller for the interactive taskbar within the poker UI. * - *

- * Responsible for: - Drag-and-drop movement of the taskbar, - Input and - * management of game + *

Responsible for: - Drag-and-drop movement of the taskbar, - Input and management of game * stakes, - Control of general menu functions such as Exit. */ public class TaskbarController { private static final Logger LOGGER = LogManager.getLogger(CasinoBrowserController.class); - @FXML - private HBox taskbar; - @FXML - private TextField taskbarInput; - @FXML - private Button betButton; - @FXML - private Button callButton; - @FXML - private Button foldButton; - @FXML - private Button raiseButton; - @FXML - private Label moneyLabel; + @FXML private HBox taskbar; + @FXML private TextField taskbarInput; + @FXML private Button betButton; + @FXML private Button callButton; + @FXML private Button foldButton; + @FXML private Button raiseButton; + @FXML private Label moneyLabel; private GameService gameService; private PlayerId myPlayerId; @@ -66,12 +57,10 @@ public class TaskbarController { } /** - * Updates the state of the basic action buttons (Bet, Call, Fold, Raise) and - * the input field + * Updates the state of the basic action buttons (Bet, Call, Fold, Raise) and the input field * * @param isMyTurn Indicates whether it is currently the player's turn. - * @param isOut Indicates whether the player is currently out of the game - * (folded or all-in). + * @param isOut Indicates whether the player is currently out of the game (folded or all-in). */ private void updateBasicButtons(boolean isMyTurn, boolean isOut) { @@ -100,8 +89,7 @@ public class TaskbarController { } /** - * Sets the given button to a disabled state with red styling, indicating that - * the player is out + * Sets the given button to a disabled state with red styling, indicating that the player is out * * @param b The button to be styled as red and disabled */ @@ -117,8 +105,7 @@ public class TaskbarController { } /** - * Sets the given input field to a disabled state with red styling, indicating - * that the player + * Sets the given input field to a disabled state with red styling, indicating that the player * is out * * @param t The text field to be styled as red and disabled @@ -135,8 +122,7 @@ public class TaskbarController { } /** - * Activates the given button by enabling it and applying yellow styling, - * indicating that it is + * Activates the given button by enabling it and applying yellow styling, indicating that it is * the player's turn * * @param b The button to be activated and styled for the player's turn @@ -152,8 +138,7 @@ public class TaskbarController { } /** - * Activates the given input field by enabling it and applying yellow styling, - * indicating that + * Activates the given input field by enabling it and applying yellow styling, indicating that * it is the player's turn * * @param t The text field to be activated and styled for the player's turn @@ -169,8 +154,7 @@ public class TaskbarController { } /** - * Deactivates the given button by disabling it and applying gray styling, - * indicating that it is + * Deactivates the given button by disabling it and applying gray styling, indicating that it is * not the player's turn * * @param b The button to be deactivated and styled for non-active state @@ -187,8 +171,7 @@ public class TaskbarController { } /** - * Deactivates the given input field by disabling it and applying gray styling, - * indicating that + * Deactivates the given input field by disabling it and applying gray styling, indicating that * it is not the player's turn * * @param t The text field to be deactivated and styled for non-active state @@ -205,8 +188,7 @@ public class TaskbarController { } /** - * Updates the displayed amount of money the player has. The amount is shown in - * the format "X$". + * Updates the displayed amount of money the player has. The amount is shown in the format "X$". * * @param amount The amount of money to display for the player. */ @@ -220,8 +202,7 @@ public class TaskbarController { } /** - * Initializes the taskbar controller. This method is called automatically after - * the FXML + * Initializes the taskbar controller. This method is called automatically after the FXML * components have been loaded. */ @FXML @@ -230,10 +211,8 @@ public class TaskbarController { } /** - * Updates the taskbar based on the current game state and the player's status. - * It checks if - * it's the player's turn and whether they are out of the game (folded or - * all-in) to adjust the + * Updates the taskbar based on the current game state and the player's status. It checks if + * it's the player's turn and whether they are out of the game (folded or all-in) to adjust the * button states and displayed money accordingly. * * @param state @@ -246,10 +225,11 @@ public class TaskbarController { return; } - Player me = state.players.stream() - .filter(p -> myPlayerId.equals(p.getId())) - .findFirst() - .orElse(null); + Player me = + state.players.stream() + .filter(p -> myPlayerId.equals(p.getId())) + .findFirst() + .orElse(null); if (me == null) { LOGGER.error("Player not found in GameState!"); @@ -266,8 +246,7 @@ public class TaskbarController { } /** - * Called when the taskbar is clicked with the mouse. Saves the relative - * position for later, + * Called when the taskbar is clicked with the mouse. Saves the relative position for later, * correct repositioning. * * @param event The mouse event @@ -279,13 +258,10 @@ public class TaskbarController { } /** - * Called while dragging the taskbar with the mouse. Updates the position and - * slightly scales + * Called while dragging the taskbar with the mouse. Updates the position and slightly scales * the taskbar for visual feedback. * - *

- * TODO: It still needs to be fixed that the taskbar cannot disappear out of the - * window. + *

TODO: It still needs to be fixed that the taskbar cannot disappear out of the window. * * @param event Das Mausereignis */ @@ -299,8 +275,7 @@ public class TaskbarController { } /** - * Called when the mouse cursor is released over the taskbar. Resets the taskbar - * scaling to + * Called when the mouse cursor is released over the taskbar. Resets the taskbar scaling to * normal size. * * @param event The mouse event @@ -324,8 +299,7 @@ public class TaskbarController { } /** - * Called when the submit button in the taskbar is pressed. Triggers the - * processing of the + * Called when the submit button in the taskbar is pressed. Triggers the processing of the * deployment. */ @FXML @@ -386,8 +360,7 @@ public class TaskbarController { } /** - * Refreshes the game state by fetching the latest state from the GameService - * and updating the + * Refreshes the game state by fetching the latest state from the GameService and updating the * taskbar accordingly. */ private void refreshGame() { @@ -402,28 +375,32 @@ public class TaskbarController { } /** - * Called when the Exit button is clicked. Closes the current game stage and - * opens the lobby UI. + * Called when the Exit button is clicked. Closes the current game stage and opens the lobby UI. */ @FXML private void onExitButtonClick() { javafx.application.Platform.runLater( () -> { // Close game stage - javafx.stage.Stage currentStage = (javafx.stage.Stage) taskbar.getScene().getWindow(); + javafx.stage.Stage currentStage = + (javafx.stage.Stage) taskbar.getScene().getWindow(); currentStage.close(); // Start lobby UI try { javafx.stage.Stage newStage = new javafx.stage.Stage(); - javafx.fxml.FXMLLoader fxmlLoader = new javafx.fxml.FXMLLoader( - getClass().getResource("/ui-structure/Casinomainui.fxml")); + javafx.fxml.FXMLLoader fxmlLoader = + new javafx.fxml.FXMLLoader( + getClass().getResource("/ui-structure/Casinomainui.fxml")); javafx.scene.Parent root = fxmlLoader.load(); - javafx.scene.Scene scene = new javafx.scene.Scene(root, 1200, 800); + javafx.scene.Scene scene = + new javafx.scene.Scene( + root, Casinomainui.SCENE_WIDTH, Casinomainui.SCENE_HEIGHT); newStage.setTitle("Casono"); - javafx.scene.image.Image icon = new javafx.scene.image.Image( - getClass() - .getResource("/images/logoinverted.png") - .toExternalForm()); + javafx.scene.image.Image icon = + new javafx.scene.image.Image( + getClass() + .getResource("/images/logoinverted.png") + .toExternalForm()); newStage.getIcons().add(icon); newStage.setScene(scene); newStage.setFullScreen(true); @@ -435,10 +412,8 @@ public class TaskbarController { } /** - * Processes the stake entered in the text field. Only integer values between 5 - * and 100,000 - * credits are accepted, in multiples of 5 (in increments of 5). The stake is - * currently only + * Processes the stake entered in the text field. Only integer values between 5 and 100,000 + * credits are accepted, in multiples of 5 (in increments of 5). The stake is currently only * displayed on the console. */ private void processBet() { @@ -476,9 +451,7 @@ public class TaskbarController { /** * Opens the integrated Casono web browser. * - *

- * TODO: Replace the start URL with the official project website (e.g., Tips & - * Tricks page) + *

TODO: Replace the start URL with the official project website (e.g., Tips & Tricks page) * once the content for strategies and support is available. */ @FXML diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/Casinomainui.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/Casinomainui.java index bb7976c..67415c5 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/Casinomainui.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/Casinomainui.java @@ -19,8 +19,8 @@ public class Casinomainui extends Application { // Default constructor } - private static final int SCENE_WIDTH = 1200; - private static final int SCENE_HEIGHT = 800; + public static final int SCENE_WIDTH = 1200; + public static final int SCENE_HEIGHT = 800; @Override /** From 9044ce366084bee8cb50c0e05629cb403b9bc3d4 Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Sun, 12 Apr 2026 13:31:00 +0200 Subject: [PATCH 16/20] Fix: Allow primitive values to contain hyphens (UUIDs) in addition to digits/words/colons --- .../dmi/dbis/cs108/casono/client/network/ClientService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java index e6b7643..8fac6f3 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java @@ -80,9 +80,11 @@ public class ClientService { return offlineMode; } + // Allow primitive values to contain hyphens (UUIDs) in addition to + // digits/words/colons static Pattern responseRex = Pattern.compile( - "(?\\w+)=(('(?([^']|\\')+)')|(?[+-]?[\\d\\w:]+))"); + "(?\\w+)=(('(?([^']|\\')+)')|(?[+-]?[-\\d\\w:]+))"); /** * Removes escape characters from a string, specifically converting escaped single quotes (\') From 346a728fb9f778bd5e8062ffc2e582377a6acdc3 Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Sun, 12 Apr 2026 13:53:23 +0200 Subject: [PATCH 17/20] Feat): Add STATUS param to GET_LOBBY_STATUS and client parsing --- .../casono/client/network/LobbyClient.java | 20 ++++++++++++++++++- .../GetLobbyStatusResponse.java | 7 +++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/LobbyClient.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/LobbyClient.java index 9f6f4a8..8687873 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/LobbyClient.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/LobbyClient.java @@ -32,7 +32,25 @@ public class LobbyClient { * @return A string representing the current status of the lobby, as returned by the server. */ public String fetchLobbyStatusString(int lobbyId) { - return client.processCommand("GET_LOBBY_STATUS ID=" + lobbyId).getFirst(); + List lines = client.processCommand("GET_LOBBY_STATUS ID=" + lobbyId); + + // Prefer an explicit STATUS parameter if provided by the server + List params = ClientService.convertToRequestParameters(lines); + for (RequestParameter p : params) { + if ("STATUS".equalsIgnoreCase(p.key())) { + return p.value(); + } + } + + // Fallback: look for plain status tokens in the body + for (String l : lines) { + String t = l.trim(); + if ("CREATED".equalsIgnoreCase(t) || "RUNNING".equalsIgnoreCase(t)) { + return t; + } + } + + return null; } /** diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/get_lobby_status/GetLobbyStatusResponse.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/get_lobby_status/GetLobbyStatusResponse.java index 043eb3d..11f0149 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/get_lobby_status/GetLobbyStatusResponse.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/get_lobby_status/GetLobbyStatusResponse.java @@ -26,6 +26,13 @@ public class GetLobbyStatusResponse extends SuccessResponse { "LOBBY", lb -> { lb.param("ID", lobby.getId().value()); + // STATUS indicates whether a game has been started in this + // lobby + lb.param( + "STATUS", + lobby.getGameController() == null + ? "CREATED" + : "RUNNING"); lb.param("NAME", lobby.getName()); lb.block( "PLAYERS", From bc36892699f1286c1cdf3da86a698a36230fbfa8 Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Sun, 12 Apr 2026 14:40:17 +0200 Subject: [PATCH 18/20] Feat: Expiry broadcast + client event handling and robustness --- .../casono/client/network/ClientService.java | 249 ++++++++++++------ .../casono/client/network/LobbyClient.java | 31 ++- .../ui/lobbyui/LobbyButtonGridManager.java | 65 ++++- .../dbis/cs108/casono/server/ServerApp.java | 42 +++ .../GetLobbyStatusResponse.java | 1 + 5 files changed, 300 insertions(+), 88 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java index 8fac6f3..86b4089 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java @@ -7,11 +7,17 @@ import java.io.IOException; import java.net.Socket; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.logging.log4j.LogManager; @@ -30,10 +36,17 @@ public class ClientService { private final ExecutorService executor; private final boolean offlineMode; - public static ArrayList response; private final AtomicInteger idGenerator; private Logger logger; + private final Map> pendingResponses = + new ConcurrentHashMap<>(); + private final CopyOnWriteArrayList>> eventListeners = + new CopyOnWriteArrayList<>(); + private Thread readerThread = null; + private final AtomicBoolean running = new AtomicBoolean(false); + private static final int READER_JOIN_TIMEOUT_MS = 500; + /** * Constructs a ClientService with the given server IP and port. It establishes a socket * connection to the server and initializes the TcpTransport and ExecutorService for @@ -43,7 +56,6 @@ public class ClientService { * @param port The port number of the server to connect to. */ public ClientService(String ip, int port) { - this.idGenerator = new AtomicInteger(0); this.logger = LogManager.getLogger(ClientService.class); @@ -59,6 +71,100 @@ public class ClientService { } executor = Executors.newSingleThreadExecutor(); + + startReaderThread(); + } + + private void startReaderThread() { + running.set(true); + readerThread = + new Thread( + () -> { + while (running.get()) { + try { + RawPacket rp = clienttcptransport.read(); + processRawPacket(rp); + } catch (IOException e) { + if (running.get()) { + logger.warn("IO error on transport reader", e); + } + break; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + }, + "casono-client-reader"); + readerThread.setDaemon(true); + readerThread.start(); + } + + private void processRawPacket(RawPacket rp) throws InterruptedException, IOException { + int rid = rp.requestId(); + String responseText = rp.payload(); + logger.info("Raw message '{}'", responseText); + + boolean hasStatus = false; + boolean success = false; + List lines = new ArrayList<>(); + + for (String rawLine : responseText.split("\\n")) { + String line = rawLine; + if (!hasStatus) { + if ("+OK".equals(line)) { + success = true; + hasStatus = true; + continue; + } + if (line.startsWith("-ERR") || line.startsWith("-ERROR")) { + success = false; + hasStatus = true; + continue; + } + continue; + } + + if ("END".equals(line)) { + break; + } + + if (line.startsWith("\t")) { + line = line.substring(1); + } + lines.add(line); + } + + if (!hasStatus) { + if (responseText.contains("+OK")) { + success = true; + hasStatus = true; + } else if (responseText.contains("-ERR") || responseText.contains("-ERROR")) { + success = false; + hasStatus = true; + } else { + // best effort: treat as success + success = true; + hasStatus = true; + } + } + + if (rid == 0) { + for (Consumer> l : eventListeners) { + try { + l.accept(List.copyOf(lines)); + } catch (Exception e) { + logger.warn("Event listener threw", e); + } + } + } else { + ArrayBlockingQueue q = pendingResponses.get(rid); + if (q != null) { + q.put(new ParsedResponse(success, lines)); + } else { + logger.warn("No pending response queue for id {}", rid); + } + } } /** @@ -124,6 +230,19 @@ public class ClientService { .toList(); } + private static record ParsedResponse(boolean success, List lines) {} + + /** + * Register an event listener that receives unsolicited event payload lines (no status prefix). + */ + public void addEventListener(Consumer> listener) { + eventListeners.add(listener); + } + + public void removeEventListener(Consumer> listener) { + eventListeners.remove(listener); + } + /** * Sends a command to the server and processes the multi-line response. It handles the protocol * handshake (expecting +OK), strips leading tabs from response lines, and collects them until @@ -135,74 +254,51 @@ public class ClientService { * occurs. */ protected List processCommand(String message) { - List response = new ArrayList<>(); - sendRequest( - () -> { - try { - writeToTransport(message); - String responseText = clienttcptransport.read().payload(); - logger.info("Raw message '{}'", responseText); + if (offlineMode) { + throw new RuntimeException("ClientService is offline"); + } - boolean hasStatus = false; - boolean success = false; - for (String rawLine : responseText.split("\n")) { - String line = rawLine; - if (!hasStatus) { - if ("+OK".equals(line)) { - success = true; - hasStatus = true; - continue; - } - if (line.startsWith("-ERR") || line.startsWith("-ERROR")) { - success = false; - hasStatus = true; - continue; - } - // ignore any lines before the status indicator - continue; + int reqId = idGenerator.incrementAndGet(); + ArrayBlockingQueue q = new ArrayBlockingQueue<>(1); + pendingResponses.put(reqId, q); + + Future writeFuture = + executor.submit( + () -> { + try { + clienttcptransport.write(new RawPacket(reqId, message)); + } catch (IOException e) { + throw new RuntimeException(e); } + }); - if ("END".equals(line)) { - break; - } + try { + writeFuture.get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + pendingResponses.remove(reqId); + throw new RuntimeException(e); + } catch (ExecutionException e) { + pendingResponses.remove(reqId); + throw getRuntimeException(e); + } - // strip a single leading tab if present (protocol formatting) - if (line.startsWith("\t")) { - line = line.substring(1); - } - response.add(line); - } + ParsedResponse pr; + try { + pr = q.take(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + pendingResponses.remove(reqId); + throw new RuntimeException(e); + } finally { + pendingResponses.remove(reqId); + } - if (!hasStatus) { - // Fallback for servers that do not place the status on a - // dedicated line: try to detect status markers anywhere - // in the payload to remain compatible with older servers. - if (responseText.contains("+OK")) { - success = true; - hasStatus = true; - } else if (responseText.contains("-ERR") - || responseText.contains("-ERROR")) { - success = false; - hasStatus = true; - } else { - throw new RuntimeException( - "No status line in response for '" - + message - + "': " - + responseText); - } - } + if (pr.success) { + return pr.lines; + } - if (success) { - return; - } - - throw new RuntimeException("Error in " + message + ": " + response); - } catch (Exception e) { - throw getRuntimeException(e); - } - }); - return response; + throw new RuntimeException("Error in " + message + ": " + pr.lines); } /** @@ -250,7 +346,17 @@ public class ClientService { */ public void closeSocket() { try { - executor.shutdown(); + running.set(false); + if (readerThread != null) { + readerThread.interrupt(); + try { + readerThread.join(READER_JOIN_TIMEOUT_MS); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + } + + executor.shutdownNow(); clienttcptransport.close(); socket.close(); logger.info("Socket closed"); @@ -259,16 +365,9 @@ public class ClientService { } } - /** - * Method to write with the tcp transport to the server - * - * @param s - Message to be sent - * @throws IOException - */ - private void writeToTransport(String s) throws IOException { - int id = this.idGenerator.incrementAndGet(); - this.clienttcptransport.write(new RawPacket(id, s)); - } + // removed unused helper writeToTransport; write is done via processCommand() + // which + // manages request ids and response matching public void ping() { processCommand("PING"); diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/LobbyClient.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/LobbyClient.java index 9f6f4a8..2f48eef 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/LobbyClient.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/LobbyClient.java @@ -32,7 +32,25 @@ public class LobbyClient { * @return A string representing the current status of the lobby, as returned by the server. */ public String fetchLobbyStatusString(int lobbyId) { - return client.processCommand("GET_LOBBY_STATUS ID=" + lobbyId).getFirst(); + List lines = client.processCommand("GET_LOBBY_STATUS ID=" + lobbyId); + + // Prefer explicit STATUS parameter when available + List params = ClientService.convertToRequestParameters(lines); + for (RequestParameter p : params) { + if ("STATUS".equalsIgnoreCase(p.key())) { + return p.value(); + } + } + + // Fallback: some servers may return a plain token as the first line + if (!lines.isEmpty()) { + String first = lines.get(0).trim(); + if ("CREATED".equalsIgnoreCase(first) || "RUNNING".equalsIgnoreCase(first)) { + return first.toUpperCase(); + } + } + + return null; } /** @@ -68,8 +86,15 @@ public class LobbyClient { * @return The id of the lobby that the client is currently in, as returned by the server. */ public int getLobbyId() { - String response = client.processCommand("GET_LOBBY_ID").getFirst(); - return Integer.parseInt(response); + List lines = client.processCommand("GET_LOBBY_ID"); + if (lines.isEmpty()) { + throw new RuntimeException("GET_LOBBY_ID returned empty response"); + } + try { + return Integer.parseInt(lines.get(0).trim()); + } catch (NumberFormatException e) { + throw new RuntimeException("Invalid GET_LOBBY_ID response: " + lines, e); + } } /** diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java index 55bbdca..397d723 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java @@ -2,6 +2,7 @@ package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui; import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService; import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient; +import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -53,7 +54,7 @@ public class LobbyButtonGridManager { this.translationManager = LobbyButtonTranslationManager.getInstance(); this.lobbyClient = lobbyClient; - startPeriodicRefresh(REFRESH_INTERVAL_SECONDS, INITIAL_DELAY_SECONDS); + startPeriodicRefresh(INITIAL_DELAY_SECONDS, REFRESH_INTERVAL_SECONDS); } public LobbyButtonGridManager( @@ -62,6 +63,43 @@ public class LobbyButtonGridManager { ClientService clientService) { this(gridPane, translationManager, new LobbyClient(clientService)); + + // Subscribe to server-initiated events so closed lobbies are removed + // immediately + clientService.addEventListener( + lines -> { + List params = ClientService.convertToRequestParameters(lines); + String evt = null; + String lidStr = null; + for (RequestParameter p : params) { + if ("EVENT".equalsIgnoreCase(p.key())) { + evt = p.value(); + } else if ("LOBBY_ID".equalsIgnoreCase(p.key())) { + lidStr = p.value(); + } + } + if (evt != null && "LOBBY_CLOSED".equalsIgnoreCase(evt) && lidStr != null) { + int lid; + try { + lid = Integer.parseInt(lidStr); + } catch (NumberFormatException ex) { + return; + } + // find button id(s) for this lobby and remove mapping + Map mapping = translationManager.getButtonIdToLobbyId(); + Integer toRemove = null; + for (Map.Entry e : mapping.entrySet()) { + if (e.getValue() != null && e.getValue().intValue() == lid) { + toRemove = e.getKey(); + break; + } + } + if (toRemove != null) { + translationManager.removeLobbyButton(toRemove); + javafx.application.Platform.runLater(this::renderLobbyButtons); + } + } + }); } private void startPeriodicRefresh(long initialDelay, long period) { @@ -80,7 +118,11 @@ public class LobbyButtonGridManager { for (Map.Entry e : entries) { int buttonId = e.getKey(); - int lobbyId = e.getValue(); + Integer lobbyIdObj = e.getValue(); + if (lobbyIdObj == null) { + continue; + } + int lobbyId = lobbyIdObj.intValue(); CompletableFuture.supplyAsync( () -> { @@ -97,8 +139,7 @@ public class LobbyButtonGridManager { if (status == null) { translationManager.removeLobbyButton(buttonId); - javafx.application.Platform.runLater( - this::updateLobbyButtonImages); + javafx.application.Platform.runLater(this::renderLobbyButtons); } }); } @@ -118,7 +159,11 @@ public class LobbyButtonGridManager { for (int index = 0; index < buttonIds.size(); index++) { Integer buttonId = buttonIds.get(index); - int lobbyId = mapping.get(buttonId); + Integer lobbyIdObj = mapping.get(buttonId); + if (lobbyIdObj == null) { + continue; + } + int lobbyId = lobbyIdObj.intValue(); Button btn = createLobbyButton(buttonId, lobbyId); @@ -253,7 +298,11 @@ public class LobbyButtonGridManager { } for (Integer buttonId : mapping.keySet()) { - int lobbyId = mapping.get(buttonId); + Integer lobbyIdObj = mapping.get(buttonId); + if (lobbyIdObj == null) { + continue; + } + int lobbyId = lobbyIdObj.intValue(); CompletableFuture.supplyAsync( () -> { @@ -361,8 +410,4 @@ public class LobbyButtonGridManager { public LobbyClient getLobbyClient() { return lobbyClient; } - - public void refreshNow() { - refreshMappings(); - } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java index c0365c3..e1cf1d6 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java @@ -33,7 +33,11 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandR import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParserDispatcher; import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent; import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher; +import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.Session; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionDisconnectJob; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager; import java.time.Duration; @@ -51,6 +55,9 @@ public class ServerApp { private static final int SESSION_DISCONNECT_JOB_DELAY = 0; private static final int SESSION_DISCONNECT_JOB_PERIOD = 2; private static final int SESSION_DISCONNECT_JOB_TIMEOUT = 5; + private static final int LOBBY_EXPIRY_SECONDS = 30; + private static final int LOBBY_CLEANUP_INITIAL_DELAY_SECONDS = 5; + private static final int LOBBY_CLEANUP_PERIOD_SECONDS = 5; public static void start(String arg) { int port = Integer.parseInt(arg); @@ -89,6 +96,41 @@ public class ServerApp { LobbyManager lobbyManager = new LobbyManager(); registerCommands(dispatcher, router, responseDispatcher, userRegistry, lobbyManager); + // Periodic cleanup: remove empty lobbies older than 30s and notify affected + // users + scheduler.scheduleAtFixedRate( + () -> { + try { + var expired = + lobbyManager.findEmptyLobbiesOlderThan( + Duration.ofSeconds(LOBBY_EXPIRY_SECONDS)); + for (var lid : expired) { + // remove lobby from manager first + lobbyManager.removeLobby(lid); + + // broadcast LOBBY_CLOSED event to all connected sessions + // (requestId=0) + for (Session s : sessionManager.getAllSessions()) { + RequestContext ctx = new RequestContext(s.getId(), 0); + SuccessResponse ev = + new SuccessResponse( + ctx, + ResponseBody.builder() + .param("EVENT", "LOBBY_CLOSED") + .param("LOBBY_ID", lid.value()) + .build()) {}; + + responseDispatcher.dispatch(ev); + } + } + } catch (Exception e) { + logger.warn("Lobby expiry job failed", e); + } + }, + LOBBY_CLEANUP_INITIAL_DELAY_SECONDS, + LOBBY_CLEANUP_PERIOD_SECONDS, + TimeUnit.SECONDS); + NetworkManager networkManager = new NetworkManager(port, sessionManager, router); networkManager.start(); } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/get_lobby_status/GetLobbyStatusResponse.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/get_lobby_status/GetLobbyStatusResponse.java index 043eb3d..15d82fb 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/get_lobby_status/GetLobbyStatusResponse.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/get_lobby_status/GetLobbyStatusResponse.java @@ -22,6 +22,7 @@ public class GetLobbyStatusResponse extends SuccessResponse { super( context, ResponseBody.builder() + .param("STATUS", lobby.getGameController() != null ? "RUNNING" : "CREATED") .block( "LOBBY", lb -> { From 3e1d787b69aac875941a782dfd3d8fcac99c206f Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Sun, 12 Apr 2026 14:52:30 +0200 Subject: [PATCH 19/20] Feat: Add findEmptyLobbiesOlderThan to find and remove stale empty lobbies --- .../server/domain/lobby/LobbyManager.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyManager.java index 515aee4..cdeb574 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyManager.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyManager.java @@ -1,6 +1,9 @@ package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby; import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby.AddResult; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; @@ -17,6 +20,7 @@ public class LobbyManager { private final Map activeLobbies = new ConcurrentHashMap<>(); private final Map playerToLobby = new ConcurrentHashMap<>(); + private final Map creationTimes = new ConcurrentHashMap<>(); private final List listeners = new CopyOnWriteArrayList<>(); private final int maxPlayersPerLobby; private static final Logger LOGGER = Logger.getLogger(LobbyManager.class.getName()); @@ -39,6 +43,7 @@ public class LobbyManager { if (!activeLobbies.containsKey(id)) { Lobby lobby = new Lobby(id, name == null ? ("Room " + i) : name); activeLobbies.put(id, lobby); + creationTimes.put(id, Instant.now()); return id; } } @@ -136,6 +141,35 @@ public class LobbyManager { return activeLobbies.values(); } + /** Find all empty lobbies that were created more than the given {@code age} ago. */ + public List findEmptyLobbiesOlderThan(Duration age) { + List result = new ArrayList<>(); + Instant cutoff = Instant.now().minus(age); + for (Map.Entry e : activeLobbies.entrySet()) { + LobbyId id = e.getKey(); + Lobby l = e.getValue(); + if (l.getPlayerNames().isEmpty()) { + Instant created = creationTimes.get(id); + if (created != null && created.isBefore(cutoff)) { + result.add(id); + } + } + } + return result; + } + + /** Remove a lobby and clean up internal mappings. */ + public void removeLobby(LobbyId id) { + Lobby removed = activeLobbies.remove(id); + creationTimes.remove(id); + if (removed == null) { + return; + } + for (String username : removed.getPlayerNames()) { + playerToLobby.remove(username); + } + } + /** * Apply the given action to every player username in the lobby identified by {@code lobbyId}. * This is a small helper that keeps iteration logic centralized and avoids leaking internal From e08b75dc7a21d1a06f4852cd8601b4772c396152 Mon Sep 17 00:00:00 2001 From: Mathis Ginkel Date: Sun, 12 Apr 2026 15:31:59 +0200 Subject: [PATCH 20/20] Style: Apply Spotless --- .../casono/client/chat/ChatController.java | 58 ++++++++----- .../casono/client/network/ChatClient.java | 11 ++- .../client/ui/chatui/ChatBoxController.java | 81 +++++++++++-------- .../casono/client/chat/ChatApplication.java | 3 - 4 files changed, 96 insertions(+), 57 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatController.java index 5c8b50a..3a69a45 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatController.java @@ -3,10 +3,12 @@ package ch.unibas.dmi.dbis.cs108.casono.client.chat; import ch.unibas.dmi.dbis.cs108.casono.client.network.ChatClient; import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService; import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController; - -import java.util.*; - -import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jspecify.annotations.Nullable; @@ -43,9 +45,10 @@ public class ChatController { private static final long REFRESH_TIME = 1000; - private List localUserList; + /** List of all users connected to the server, to safe them locally on the client */ + private final List localUserList; - private Logger logger; + private final Logger logger; /** * Constructor, adds TimerTask to be sent to the server regularly @@ -74,7 +77,7 @@ public class ChatController { } /** - * Method to be activated, if a lobby has be chosen. It will update the UI and add a new + * Method to be activated, if a lobby has been chosen. It will update the UI and add a new * ChatModel to hold the Data for the Lobby Chat * * @param lobbyId @@ -86,7 +89,15 @@ public class ChatController { this.chatBoxController.addChatTab("Lobby", lobbyChatModel); } - /** method to get all messages from the server */ + /** + * Method to process the Messages that got polled by the {@link ChatClient}. + * + *

Case distinction for every message: - Messages identified with ChatType.LOBBY -> check if + * they belong to the users lobby. - Messages identified with ChatType.GLOBAL -> check if the + * target user of that Message is the user, or the message got sent by the user. + * + *

All Messages get added to a particular {@link ChatModel}, if the checks passed. + */ public void receiveMessage() { List newMessages = chatClient.getMessages(); if (!newMessages.isEmpty()) { @@ -112,17 +123,20 @@ public class ChatController { case ChatType.WHISPER: if (msg.target.equals(username) || msg.sender.equals(username)) { ChatKey key; - if(msg.target.equals(username)) { - key = new ChatKey(ChatType.WHISPER, msg.sender); - } else { - key = new ChatKey(ChatType.WHISPER, msg.target); - } - if (chatModelMap.containsKey(key)) { - chatModelMap - .get(key) - .addMessage(msg); + if (msg.target.equals(username)) { + key = new ChatKey(ChatType.WHISPER, msg.sender); } else { - ChatModel chatModel = new ChatModel(ChatType.WHISPER, username, lobbyId, key.targetUser()); + key = new ChatKey(ChatType.WHISPER, msg.target); + } + if (chatModelMap.containsKey(key)) { + chatModelMap.get(key).addMessage(msg); + } else { + ChatModel chatModel = + new ChatModel( + ChatType.WHISPER, + username, + lobbyId, + key.targetUser()); chatBoxController.addWhisperChat(key.targetUser(), chatModel); chatModel.addMessage(msg); } @@ -133,6 +147,14 @@ public class ChatController { } } + /** + * Method to process the usernames of other users connected to the server, after they got polled + * by the {@link ChatClient}. + * + *

Checks for every one of the usernames, if it is already in the list localUserList and if + * not adds them. For every new User added in the List, they get added as an option to start a + * Whisper Chat with. + */ public synchronized void checkWhisperUsers() { List users = chatClient.getUsers(); logger.info(users); diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ChatClient.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ChatClient.java index e6fe86e..6a88ed9 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ChatClient.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ChatClient.java @@ -2,9 +2,8 @@ package ch.unibas.dmi.dbis.cs108.casono.client.network; import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message; import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter; - -import java.util.*; - +import java.util.ArrayList; +import java.util.List; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -71,6 +70,12 @@ public class ChatClient { return messages; } + /** + * Method to poll the usernames of all users currently connected to the server, by sending the + * "LIST_USERS" command. + * + * @return List of all usernames retrieved from the server. + */ public List getUsers() { logger.info("Asking server for list of users"); List users = clientService.processCommand("LIST_USERS"); diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatBoxController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatBoxController.java index 3e5c518..e0a62e6 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatBoxController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatBoxController.java @@ -11,13 +11,14 @@ import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; - import javafx.application.Platform; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.Node; -import javafx.scene.control.*; +import javafx.scene.control.MenuButton; +import javafx.scene.control.MenuItem; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; @@ -77,7 +78,15 @@ public class ChatBoxController { () -> { MenuItem menuItem = new MenuItem(targetUserName); addWhisperChatButton.getItems().add(menuItem); - menuItem.setOnAction(event -> addWhisperChat(targetUserName, new ChatModel(ChatType.WHISPER, username, -1, targetUserName))); + menuItem.setOnAction( + event -> + addWhisperChat( + targetUserName, + new ChatModel( + ChatType.WHISPER, + username, + -1, + targetUserName))); }); } @@ -111,30 +120,36 @@ public class ChatBoxController { public void addChatTab(String title, ChatModel chatModel) { URL resource = getClass().getResource(ressource); FXMLLoader fxmlLoader = new FXMLLoader(resource); - runOnPlatformSynchronized(()-> { - try { - ChatViewController chatViewController = - new ChatViewController(this.chatController, chatModel, this.username); - fxmlLoader.setController(chatViewController); - Node load = fxmlLoader.load(); - VBox.setVgrow(load, Priority.ALWAYS); - VBox vbox = new VBox(); - VBox.setVgrow(vbox, Priority.ALWAYS); - vbox.getChildren().add(load); - chatModel.addListener((msg) -> chatViewController.showMessage(msg)); - Tab newChat = new Tab(title, vbox); - usernameTabMap.put(title, newChat); - this.chatTabPane.getTabs().add(newChat); - this.chatTabPane.getSelectionModel().select(newChat); - return newChat; - } catch (IOException e) { - throw new RuntimeException(e); - } - }); + runOnPlatformSynchronized( + () -> { + try { + ChatViewController chatViewController = + new ChatViewController( + this.chatController, chatModel, this.username); + fxmlLoader.setController(chatViewController); + Node load = fxmlLoader.load(); + VBox.setVgrow(load, Priority.ALWAYS); + VBox vbox = new VBox(); + VBox.setVgrow(vbox, Priority.ALWAYS); + vbox.getChildren().add(load); + chatModel.addListener((msg) -> chatViewController.showMessage(msg)); + Tab newChat = new Tab(title, vbox); + usernameTabMap.put(title, newChat); + this.chatTabPane.getTabs().add(newChat); + this.chatTabPane.getSelectionModel().select(newChat); + return newChat; + } catch (IOException e) { + throw new RuntimeException(e); + } + }); } + /** + * Helper Function to cope with a Threads problem occurring when the main JavaFX Thread performs + * the task to add a new whisper Chat tab, and the task is specified to runLater. + */ public static T runOnPlatformSynchronized(Callable code) { - if(Platform.isFxApplicationThread()) { + if (Platform.isFxApplicationThread()) { try { return code.call(); } catch (Exception e) { @@ -142,13 +157,14 @@ public class ChatBoxController { } } else { CompletableFuture futureLock = new CompletableFuture<>(); - Platform.runLater(() -> { - try { - futureLock.complete(code.call()); - } catch (Exception e) { - futureLock.completeExceptionally(e); - } - }); + Platform.runLater( + () -> { + try { + futureLock.complete(code.call()); + } catch (Exception e) { + futureLock.completeExceptionally(e); + } + }); try { return futureLock.get(); } catch (Throwable e) { @@ -156,5 +172,4 @@ public class ChatBoxController { } } } - } diff --git a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatApplication.java b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatApplication.java index 722b859..9c81ee7 100644 --- a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatApplication.java +++ b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatApplication.java @@ -4,12 +4,9 @@ import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService; import ch.unibas.dmi.dbis.cs108.casono.client.network.CoreClient; import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController; import java.io.IOException; -import java.util.ArrayList; import java.util.List; - import javafx.application.Application; import javafx.fxml.FXMLLoader; -import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage;