Merge branch 'feat/143-leave-lobby-command' into 'main'

Feat: Add leave-lobby command

See merge request cs108-fs26/Gruppe-13!172
This commit was merged in pull request #328.
This commit is contained in:
Jona Walpert
2026-05-13 21:17:57 +00:00
5 changed files with 252 additions and 0 deletions
@@ -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
@@ -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<LeaveLobbyRequest>)
new LeaveLobbyHandler(
responseDispatcher, context.lobbyManager(), userRegistry));
// START_GAME registration
parserDispatcher.register("START_GAME", new StartGameParser());
commandRouter.register(
@@ -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.
*
* <p>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<LeaveLobbyRequest> {
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()));
}
}
@@ -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.
*
* <p>Expected request parameters:
*
* <ul>
* <li>`ID` (int) — numeric lobby identifier (required)
* </ul>
*
* <p>The parser builds a {@link LeaveLobbyRequest} containing the parsed lobby id and the original
* request context.
*/
public class LeaveLobbyParser implements CommandParser<LeaveLobbyRequest> {
/**
* 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);
}
}
@@ -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.
*
* <p>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;
}
}