Feat: Add leave-lobby command
This commit is contained in:
@@ -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(
|
||||
|
||||
+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