From cd8aa9e70921ef2ede7b6d1f9d4d4ebbc3d0fe2c Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Wed, 13 May 2026 23:00:06 +0200 Subject: [PATCH] Feat: Add leave-lobby command --- .../networking/commands/protocol-document.md | 58 +++++++++ .../dbis/cs108/casono/server/ServerApp.java | 11 ++ .../lobby/leave_lobby/LeaveLobbyHandler.java | 116 ++++++++++++++++++ .../lobby/leave_lobby/LeaveLobbyParser.java | 33 +++++ .../lobby/leave_lobby/LeaveLobbyRequest.java | 34 +++++ 5 files changed, 252 insertions(+) create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/leave_lobby/LeaveLobbyHandler.java create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/leave_lobby/LeaveLobbyParser.java create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/leave_lobby/LeaveLobbyRequest.java diff --git a/documents/docs/networking/commands/protocol-document.md b/documents/docs/networking/commands/protocol-document.md index f267047..633dd16 100644 --- a/documents/docs/networking/commands/protocol-document.md +++ b/documents/docs/networking/commands/protocol-document.md @@ -102,6 +102,14 @@ 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) + - [LEAVE_LOBBY command](#leave_lobby-command) + - [Required pre-execution checks](#required-pre-execution-checks-1) + - [Request Parameters](#request-parameters-1) + - [Implementation notes](#implementation-notes) + - [Success Response](#success-response-1) + - [Error Response](#error-response-1) + - [Example Request](#example-request-1) + - [Example Response](#example-response-1) - [START_GAME command](#start_game-command) - [GET_LOBBY_STATUS command](#get_lobby_status-command) - [Required pre-execution checks](#required-pre-execution-checks) @@ -671,6 +679,56 @@ END MSG=Lobby not found END +## LEAVE_LOBBY command + +The `LEAVE_LOBBY` command marks the currently logged-in user as absent in the specified lobby while a game is running. The lobby slot is kept reserved so the player can rejoin later. + +### 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: `LeaveLobbyParser` — reads the required `ID` parameter and builds `LeaveLobbyRequest`. +- Handler: `LeaveLobbyHandler` — resolves the lobby by id, verifies that a game is currently running, resolves the user from the session and marks the player as absent via `LobbyManager.leavePlayerFromLobby(...)`. + +### Success Response + +No additional response fields. The server replies with a simple `+OK` on success. + +### Error Response + +| Code | Description | +| :--- | :---------- | +| `MISSING_PARAMETER` | Required parameter `ID` missing (parser) | +| `USER_NOT_LOGGED_IN` | No user associated with the session (pre-execution check failed) | +| `LOBBY_NOT_FOUND` | The specified lobby id does not exist | +| `GAME_NOT_RUNNING` | The lobby does not currently have a running game | +| `NOT_IN_LOBBY` | The user is not actively present in the lobby | + +### Example Request + +``` + +LEAVE_LOBBY ID=1 + +``` + +### Example Response (success) + +``` + ++OK +END + +``` + ## START_GAME command The `START_GAME` command requests the server to start a new game in the specified lobby. The 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 51e022d..7855eee 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 @@ -48,6 +48,9 @@ import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_statu import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby.JoinLobbyHandler; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby.JoinLobbyParser; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby.JoinLobbyRequest; +import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.leave_lobby.LeaveLobbyHandler; +import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.leave_lobby.LeaveLobbyParser; +import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.leave_lobby.LeaveLobbyRequest; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game.StartGameHandler; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game.StartGameParser; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game.StartGameRequest; @@ -297,6 +300,14 @@ public class ServerApp { new JoinLobbyHandler( responseDispatcher, context.lobbyManager(), userRegistry)); + // LEAVE_LOBBY registration + parserDispatcher.register("LEAVE_LOBBY", new LeaveLobbyParser()); + commandRouter.register( + LeaveLobbyRequest.class, + (CommandHandler) + new LeaveLobbyHandler( + responseDispatcher, context.lobbyManager(), userRegistry)); + // START_GAME registration parserDispatcher.register("START_GAME", new StartGameParser()); commandRouter.register( diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/leave_lobby/LeaveLobbyHandler.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/leave_lobby/LeaveLobbyHandler.java new file mode 100644 index 0000000..2a4b16f --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/leave_lobby/LeaveLobbyHandler.java @@ -0,0 +1,116 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.leave_lobby; + +import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.UserLoggedInCheck; +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.User; +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.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 `LEAVE_LOBBY` command. + * + *

Resolves the username from the session (requires {@link UserLoggedInCheck}), looks up the + * target lobby and marks the user as absent. The user can rejoin later without losing their slot. + * On success an `+OK` response is dispatched; on failure an appropriate {@link ErrorResponse} is + * returned. + */ +public class LeaveLobbyHandler extends CommandHandler { + private final LobbyManager lobbyManager; + private final UserRegistry userRegistry; + private static final Logger LOGGER = + LogManager.getLogger(LeaveLobbyHandler.class.getSimpleName()); + + /** + * Create a new {@link LeaveLobbyHandler}. + * + * @param responseDispatcher dispatcher used to send responses back to the client + * @param lobbyManager manager providing lobby state and operations + * @param userRegistry registry to resolve session -> user mappings + */ + public LeaveLobbyHandler( + ResponseDispatcher responseDispatcher, + LobbyManager lobbyManager, + UserRegistry userRegistry) { + super(responseDispatcher); + this.lobbyManager = lobbyManager; + this.userRegistry = userRegistry; + addCheck(new UserLoggedInCheck(userRegistry)); + } + + /** + * Execute the leave-lobby request. Marks the player as absent in the lobby. + * + * @param request the parsed {@link LeaveLobbyRequest} + */ + @Override + public void execute(LeaveLobbyRequest request) { + LOGGER.info( + "LEAVE_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( + "LEAVE_LOBBY: Lobby {} not found (session={})", + request.getId(), + request.getContext().sessionId()); + responseDispatcher.dispatch( + new ErrorResponse(request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found")); + return; + } + + // Only allow leaving if game is running + if (lobby.getGameController() == null) { + LOGGER.warn( + "LEAVE_LOBBY: Game not running in lobby {} (session={})", + request.getId(), + request.getContext().sessionId()); + responseDispatcher.dispatch( + new ErrorResponse( + request.getContext(), + "GAME_NOT_RUNNING", + "Can only leave a lobby while a game is running")); + return; + } + + var maybeUser = userRegistry.getBySessionId(request.getContext().sessionId()); + if (maybeUser.isEmpty()) { + LOGGER.warn( + "LEAVE_LOBBY: No user associated with session {}", + request.getContext().sessionId()); + responseDispatcher.dispatch( + new ErrorResponse( + request.getContext(), + "USER_NOT_LOGGED_IN", + "No user associated with session")); + return; + } + + User user = maybeUser.get(); + String username = user.getName(); + + boolean ok = lobbyManager.leavePlayerFromLobby(username, lid); + if (!ok) { + LOGGER.warn( + "LEAVE_LOBBY: User '{}' failed to leave lobby {} (not in lobby or not active)", + username, + request.getId()); + responseDispatcher.dispatch( + new ErrorResponse( + request.getContext(), + "NOT_IN_LOBBY", + "You are not in this lobby or not actively playing")); + return; + } + + LOGGER.info("User '{}' left lobby {} (marked absent)", username, request.getId()); + responseDispatcher.dispatch(new OkResponse(request.getContext())); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/leave_lobby/LeaveLobbyParser.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/leave_lobby/LeaveLobbyParser.java new file mode 100644 index 0000000..fbbaa93 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/leave_lobby/LeaveLobbyParser.java @@ -0,0 +1,33 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.leave_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; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor; + +/** + * Parser for the `LEAVE_LOBBY` command. + * + *

Expected request parameters: + * + *

+ * + *

The parser builds a {@link LeaveLobbyRequest} containing the parsed lobby id and the original + * request context. + */ +public class LeaveLobbyParser implements CommandParser { + /** + * Parse the incoming primitive request into a {@link LeaveLobbyRequest}. + * + * @param primitiveRequest the raw primitive request + * @return a {@link LeaveLobbyRequest} with the parsed lobby id and context + */ + @Override + public LeaveLobbyRequest parse(PrimitiveRequest primitiveRequest) { + RequestParameterAccessor accessor = + new RequestParameterAccessor(primitiveRequest.parameters()); + int id = accessor.require("ID", Integer::parseInt); + return new LeaveLobbyRequest(primitiveRequest.context(), id); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/leave_lobby/LeaveLobbyRequest.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/leave_lobby/LeaveLobbyRequest.java new file mode 100644 index 0000000..5faea90 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/leave_lobby/LeaveLobbyRequest.java @@ -0,0 +1,34 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.leave_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; + +/** + * Request data for the `LEAVE_LOBBY` command. + * + *

Contains the lobby id the client wants to leave and inherits the {@link Request} contextual + * information. + */ +public class LeaveLobbyRequest extends Request { + private final int id; + + /** + * Create a new {@link LeaveLobbyRequest}. + * + * @param context the request context + * @param id numeric lobby id to leave + */ + public LeaveLobbyRequest(RequestContext context, int id) { + super(context); + this.id = id; + } + + /** + * Returns the lobby id requested by the client. + * + * @return numeric lobby id + */ + public int getId() { + return id; + } +}