Feat: Add leave-lobby command #328
@@ -102,6 +102,14 @@ This document describes the protocol for client-server communication in our appl
|
|||||||
- [Required pre-execution checks](#required-pre-execution-checks)
|
- [Required pre-execution checks](#required-pre-execution-checks)
|
||||||
- [Request Parameters](#request-parameters)
|
- [Request Parameters](#request-parameters)
|
||||||
- [Success Response](#success-response)
|
- [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)
|
- [START_GAME command](#start_game-command)
|
||||||
- [GET_LOBBY_STATUS command](#get_lobby_status-command)
|
- [GET_LOBBY_STATUS command](#get_lobby_status-command)
|
||||||
- [Required pre-execution checks](#required-pre-execution-checks)
|
- [Required pre-execution checks](#required-pre-execution-checks)
|
||||||
@@ -671,6 +679,56 @@ END
|
|||||||
MSG=Lobby not found
|
MSG=Lobby not found
|
||||||
END
|
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
|
## START_GAME command
|
||||||
|
|
||||||
The `START_GAME` command requests the server to start a new game in the specified lobby. The
|
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.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.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.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.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.StartGameParser;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game.StartGameRequest;
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game.StartGameRequest;
|
||||||
@@ -297,6 +300,14 @@ public class ServerApp {
|
|||||||
new JoinLobbyHandler(
|
new JoinLobbyHandler(
|
||||||
responseDispatcher, context.lobbyManager(), userRegistry));
|
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
|
// START_GAME registration
|
||||||
parserDispatcher.register("START_GAME", new StartGameParser());
|
parserDispatcher.register("START_GAME", new StartGameParser());
|
||||||
commandRouter.register(
|
commandRouter.register(
|
||||||
|
|||||||
+116
@@ -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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user