Feat: Add leave-lobby command

This commit is contained in:
Jona Walpert
2026-05-13 23:00:06 +02:00
parent 9706e140ac
commit cd8aa9e709
5 changed files with 252 additions and 0 deletions
@@ -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;
}
}