fix: correctly disaply lobbei and update them in Lobyb Screen

This commit is contained in:
Jona Walpert
2026-04-12 16:02:50 +02:00
parent e4f0c7efb8
commit 9cf6b468ad
4 changed files with 269 additions and 105 deletions
@@ -24,8 +24,10 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
/** /**
* The ClientService class is responsible for managing the connection to the server, sending * The ClientService class is responsible for managing the connection to the
* commands, and receiving responses. It uses a TcpTransport to communicate with the server and an * server, sending
* commands, and receiving responses. It uses a TcpTransport to communicate with
* the server and an
* ExecutorService to handle asynchronous requests. * ExecutorService to handle asynchronous requests.
*/ */
public class ClientService { public class ClientService {
@@ -39,17 +41,17 @@ public class ClientService {
private final AtomicInteger idGenerator; private final AtomicInteger idGenerator;
private Logger logger; private Logger logger;
private final Map<Integer, ArrayBlockingQueue<ParsedResponse>> pendingResponses = private final Map<Integer, ArrayBlockingQueue<ParsedResponse>> pendingResponses = new ConcurrentHashMap<>();
new ConcurrentHashMap<>(); private final CopyOnWriteArrayList<Consumer<List<String>>> eventListeners = new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<Consumer<List<String>>> eventListeners =
new CopyOnWriteArrayList<>();
private Thread readerThread = null; private Thread readerThread = null;
private final AtomicBoolean running = new AtomicBoolean(false); private final AtomicBoolean running = new AtomicBoolean(false);
private static final int READER_JOIN_TIMEOUT_MS = 500; private static final int READER_JOIN_TIMEOUT_MS = 500;
/** /**
* Constructs a ClientService with the given server IP and port. It establishes a socket * Constructs a ClientService with the given server IP and port. It establishes
* connection to the server and initializes the TcpTransport and ExecutorService for * a socket
* connection to the server and initializes the TcpTransport and ExecutorService
* for
* communication. * communication.
* *
* @param ip The IP address of the server to connect to. * @param ip The IP address of the server to connect to.
@@ -77,8 +79,7 @@ public class ClientService {
private void startReaderThread() { private void startReaderThread() {
running.set(true); running.set(true);
readerThread = readerThread = new Thread(
new Thread(
() -> { () -> {
while (running.get()) { while (running.get()) {
try { try {
@@ -111,13 +112,14 @@ public class ClientService {
for (String rawLine : responseText.split("\\n")) { for (String rawLine : responseText.split("\\n")) {
String line = rawLine; String line = rawLine;
String trimmed = line.trim();
if (!hasStatus) { if (!hasStatus) {
if ("+OK".equals(line)) { if ("+OK".equals(trimmed)) {
success = true; success = true;
hasStatus = true; hasStatus = true;
continue; continue;
} }
if (line.startsWith("-ERR") || line.startsWith("-ERROR")) { if (trimmed.startsWith("-ERR") || trimmed.startsWith("-ERROR")) {
success = false; success = false;
hasStatus = true; hasStatus = true;
continue; continue;
@@ -125,13 +127,12 @@ public class ClientService {
continue; continue;
} }
if ("END".equals(line)) { if ("END".equals(trimmed)) {
break; break;
} }
if (line.startsWith("\t")) { // remove leading indentation (tabs/spaces) so parameters match regex parsing
line = line.substring(1); line = line.replaceFirst("^\\s+", "");
}
lines.add(line); lines.add(line);
} }
@@ -152,6 +153,7 @@ public class ClientService {
if (rid == 0) { if (rid == 0) {
for (Consumer<List<String>> l : eventListeners) { for (Consumer<List<String>> l : eventListeners) {
try { try {
logger.debug("Parsed response lines: {}", lines);
l.accept(List.copyOf(lines)); l.accept(List.copyOf(lines));
} catch (Exception e) { } catch (Exception e) {
logger.warn("Event listener threw", e); logger.warn("Event listener threw", e);
@@ -168,7 +170,8 @@ public class ClientService {
} }
/** /**
* Constructs a ClientService in offline mode. No network connection will be attempted and calls * Constructs a ClientService in offline mode. No network connection will be
* attempted and calls
* to processCommand will throw a RuntimeException. * to processCommand will throw a RuntimeException.
* *
* @param offline true to create an offline (no-network) client service * @param offline true to create an offline (no-network) client service
@@ -181,19 +184,21 @@ public class ClientService {
this.executor = Executors.newSingleThreadExecutor(); this.executor = Executors.newSingleThreadExecutor();
} }
/** Returns true if this ClientService is running in offline mode (no network). */ /**
* Returns true if this ClientService is running in offline mode (no network).
*/
public boolean isOffline() { public boolean isOffline() {
return offlineMode; return offlineMode;
} }
// Allow primitive values to contain hyphens (UUIDs) in addition to // Allow primitive values to contain hyphens (UUIDs) in addition to
// digits/words/colons // digits/words/colons
static Pattern responseRex = static Pattern responseRex = Pattern.compile(
Pattern.compile(
"(?<key>\\w+)=(('(?<string>([^']|\\')+)')|(?<primVal>[+-]?[-\\d\\w:]+))"); "(?<key>\\w+)=(('(?<string>([^']|\\')+)')|(?<primVal>[+-]?[-\\d\\w:]+))");
/** /**
* Removes escape characters from a string, specifically converting escaped single quotes (\') * Removes escape characters from a string, specifically converting escaped
* single quotes (\')
* back to regular single quotes ('). * back to regular single quotes (').
* *
* @param input The escaped string to process. * @param input The escaped string to process.
@@ -204,8 +209,10 @@ public class ClientService {
} }
/** /**
* Converts a list of raw string parameters into a list of {@link RequestParameter} objects. It * Converts a list of raw string parameters into a list of
* uses a regex matcher to distinguish between quoted strings (which are unescaped) and * {@link RequestParameter} objects. It
* uses a regex matcher to distinguish between quoted strings (which are
* unescaped) and
* primitive values. * primitive values.
* *
* @param input A list of raw strings to be parsed. * @param input A list of raw strings to be parsed.
@@ -230,10 +237,12 @@ public class ClientService {
.toList(); .toList();
} }
private static record ParsedResponse(boolean success, List<String> lines) {} private static record ParsedResponse(boolean success, List<String> lines) {
}
/** /**
* Register an event listener that receives unsolicited event payload lines (no status prefix). * Register an event listener that receives unsolicited event payload lines (no
* status prefix).
*/ */
public void addEventListener(Consumer<List<String>> listener) { public void addEventListener(Consumer<List<String>> listener) {
eventListeners.add(listener); eventListeners.add(listener);
@@ -244,13 +253,17 @@ public class ClientService {
} }
/** /**
* Sends a command to the server and processes the multi-line response. It handles the protocol * Sends a command to the server and processes the multi-line response. It
* handshake (expecting +OK), strips leading tabs from response lines, and collects them until * handles the protocol
* handshake (expecting +OK), strips leading tabs from response lines, and
* collects them until
* the "END" marker is reached. * the "END" marker is reached.
* *
* @param message The raw command string to be sent to the transport layer. * @param message The raw command string to be sent to the transport layer.
* @return A list of response lines received from the server (excluding protocol markers). * @return A list of response lines received from the server (excluding protocol
* @throws RuntimeException if the server responds with an error or if a communication failure * markers).
* @throws RuntimeException if the server responds with an error or if a
* communication failure
* occurs. * occurs.
*/ */
protected List<String> processCommand(String message) { protected List<String> processCommand(String message) {
@@ -262,8 +275,7 @@ public class ClientService {
ArrayBlockingQueue<ParsedResponse> q = new ArrayBlockingQueue<>(1); ArrayBlockingQueue<ParsedResponse> q = new ArrayBlockingQueue<>(1);
pendingResponses.put(reqId, q); pendingResponses.put(reqId, q);
Future<?> writeFuture = Future<?> writeFuture = executor.submit(
executor.submit(
() -> { () -> {
try { try {
clienttcptransport.write(new RawPacket(reqId, message)); clienttcptransport.write(new RawPacket(reqId, message));
@@ -302,11 +314,15 @@ public class ClientService {
} }
/** /**
* Helper method to send a request to the server using the ExecutorService. It submits the * Helper method to send a request to the server using the ExecutorService. It
* request as a Runnable task and waits for its completion. If the task is interrupted or * submits the
* encounters an execution exception, it throws a RuntimeException with the appropriate cause. * request as a Runnable task and waits for its completion. If the task is
* interrupted or
* encounters an execution exception, it throws a RuntimeException with the
* appropriate cause.
* *
* @param request The Runnable task representing the request to be sent to the server. * @param request The Runnable task representing the request to be sent to the
* server.
*/ */
private void sendRequest(Runnable request) { private void sendRequest(Runnable request) {
Future<?> future = executor.submit(request); Future<?> future = executor.submit(request);
@@ -320,9 +336,12 @@ public class ClientService {
} }
/** /**
* Helper method to extract the cause of an exception and return it as a RuntimeException. If * Helper method to extract the cause of an exception and return it as a
* the cause is null, it returns the original exception as a RuntimeException. If the cause is * RuntimeException. If
* already a RuntimeException, it returns it directly. Otherwise, it wraps the cause in a new * the cause is null, it returns the original exception as a RuntimeException.
* If the cause is
* already a RuntimeException, it returns it directly. Otherwise, it wraps the
* cause in a new
* RuntimeException and returns it. * RuntimeException and returns it.
* *
* @param e The exception from which to extract the cause. * @param e The exception from which to extract the cause.
@@ -340,8 +359,10 @@ public class ClientService {
} }
/** /**
* Closes the socket connection to the server and shuts down the ExecutorService. It also closes * Closes the socket connection to the server and shuts down the
* the TcpTransport used for communication. If any IOException occurs during this process, it * ExecutorService. It also closes
* the TcpTransport used for communication. If any IOException occurs during
* this process, it
* prints the exception to the console. * prints the exception to the console.
*/ */
public void closeSocket() { public void closeSocket() {
@@ -85,6 +85,8 @@ public class LobbyButtonGridManager {
} catch (NumberFormatException ex) { } catch (NumberFormatException ex) {
return; return;
} }
LOGGER.info("LOBBY_CLOSED event for lobby {} — mapping before: {}", lid,
translationManager.getButtonIdToLobbyId());
// find button id(s) for this lobby and remove mapping // find button id(s) for this lobby and remove mapping
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId(); Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
Integer toRemove = null; Integer toRemove = null;
@@ -95,9 +97,46 @@ public class LobbyButtonGridManager {
} }
} }
if (toRemove != null) { if (toRemove != null) {
LOGGER.info("Removing mapping for button {} -> lobby {} (LOBBY_CLOSED event)", toRemove,
lid);
translationManager.removeLobbyButton(toRemove); translationManager.removeLobbyButton(toRemove);
LOGGER.debug("Mapping after removal: {}", translationManager.getButtonIdToLobbyId());
javafx.application.Platform.runLater(this::renderLobbyButtons); javafx.application.Platform.runLater(this::renderLobbyButtons);
} }
} else if (evt != null && "LOBBY_CREATED".equalsIgnoreCase(evt) && lidStr != null) {
int lid;
try {
lid = Integer.parseInt(lidStr);
} catch (NumberFormatException ex) {
return;
}
LOGGER.info("LOBBY_CREATED event for lobby {} — mapping before: {}", lid,
translationManager.getButtonIdToLobbyId());
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
// If this lobby is already known, nothing to do
if (mapping.containsValue(lid)) {
LOGGER.debug("Lobby {} already mapped, skipping", lid);
return;
}
// find next free button id (1..8)
for (int candidate = 1; candidate <= 8; candidate++) {
if (!mapping.containsKey(candidate)) {
try {
translationManager.addLobbyButton(candidate, lid);
LOGGER.info("Mapped new lobby {} to button {}", lid, candidate);
LOGGER.debug("Mapping after add: {}", translationManager.getButtonIdToLobbyId());
javafx.application.Platform.runLater(this::renderLobbyButtons);
break;
} catch (Exception ex) {
// grid full? try next candidate
LOGGER.debug("Could not map created lobby {}: {}", lid, ex.getMessage());
}
}
}
} }
}); });
} }
@@ -110,9 +149,30 @@ public class LobbyButtonGridManager {
private void refreshMappings() { private void refreshMappings() {
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId(); Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
// Always attempt to sync with server-side lobby list so clients that start
// after a lobby was created will discover it. This keeps the local mapping
// in sync with the server without requiring a restart.
List<LobbyClient.LobbyInfo> serverLobbies = null;
java.util.Set<Integer> serverIds = new java.util.HashSet<>();
try {
serverLobbies = lobbyClient.getLobbyList();
if (serverLobbies != null) {
for (var li : serverLobbies) {
serverIds.add(li.id);
}
}
reconcileWithServerLobbies(serverLobbies);
} catch (Exception ex) {
LOGGER.debug("Could not fetch lobby list: {}", ex.getMessage());
}
if (mapping.isEmpty()) { if (mapping.isEmpty()) {
// mapping may still be empty after reconciliation
if (translationManager.getButtonIdToLobbyId().isEmpty()) {
return; return;
} }
mapping = translationManager.getButtonIdToLobbyId();
}
List<Map.Entry<Integer, Integer>> entries = new ArrayList<>(mapping.entrySet()); List<Map.Entry<Integer, Integer>> entries = new ArrayList<>(mapping.entrySet());
@@ -136,15 +196,80 @@ public class LobbyButtonGridManager {
executor) executor)
.thenAccept( .thenAccept(
status -> { status -> {
// Do not remove a mapping just because the status fetch
// temporarily returned null. Only remove mappings when
// the server no longer lists the lobby (reconcileWithServerLobbies
// handles removals based on authoritative server list).
if (status == null) { if (status == null) {
if (!serverIds.contains(lobbyId)) {
translationManager.removeLobbyButton(buttonId); translationManager.removeLobbyButton(buttonId);
javafx.application.Platform.runLater(this::renderLobbyButtons); javafx.application.Platform.runLater(this::renderLobbyButtons);
} else {
// treat as created/running later; keep mapping
LOGGER.debug("Temporary missing status for lobby {} - keeping mapping",
lobbyId);
}
} }
}); });
} }
} }
private void reconcileWithServerLobbies(List<LobbyClient.LobbyInfo> serverLobbies) {
if (serverLobbies == null) {
return;
}
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
// Build set of lobby ids returned by server
java.util.Set<Integer> serverIds = new java.util.HashSet<>();
for (var li : serverLobbies) {
serverIds.add(li.id);
}
LOGGER.debug("Reconciling mappings. serverIds={} mapping={}", serverIds, mapping);
// Remove mappings for lobbies that no longer exist
java.util.List<Integer> toRemove = new java.util.ArrayList<>();
for (var e : mapping.entrySet()) {
Integer lid = e.getValue();
if (lid == null) {
continue;
}
if (!serverIds.contains(lid)) {
toRemove.add(e.getKey());
}
}
for (Integer bid : toRemove) {
LOGGER.info("Reconciling: removing mapping for button {} (server no longer lists lobby)", bid);
translationManager.removeLobbyButton(bid);
}
// Add server lobbies that are not yet mapped
for (var li : serverLobbies) {
if (mapping.containsValue(li.id)) {
continue;
}
// find next free button id (1..8)
for (int candidate = 1; candidate <= 8; candidate++) {
if (!mapping.containsKey(candidate)) {
try {
translationManager.addLobbyButton(candidate, li.id);
LOGGER.info("Reconciling: added mapping button {} -> lobby {}", candidate, li.id);
break;
} catch (Exception ex) {
// grid full? try next candidate
LOGGER.debug("Could not add mapping for lobby {}: {}", li.id, ex.getMessage());
}
}
}
}
if (!toRemove.isEmpty() || !serverLobbies.isEmpty()) {
javafx.application.Platform.runLater(this::renderLobbyButtons);
}
}
public void renderLobbyButtons() { public void renderLobbyButtons() {
gridPane.getChildren().clear(); gridPane.getChildren().clear();
@@ -215,10 +340,8 @@ public class LobbyButtonGridManager {
statusStr -> { statusStr -> {
LobbyStatus status = parseLobbyStatus(statusStr); LobbyStatus status = parseLobbyStatus(statusStr);
String path = String path = getImagePathForLobby(
getImagePathForButton( lobbyId, status == null ? LobbyStatus.CREATED : status);
buttonId,
status == null ? LobbyStatus.CREATED : status);
Image img = safeLoadImage(path); Image img = safeLoadImage(path);
@@ -253,11 +376,11 @@ public class LobbyButtonGridManager {
} }
} }
private String getImagePathForButton(int buttonId, LobbyStatus status) { private String getImagePathForLobby(int lobbyId, LobbyStatus status) {
String statusStr = status == LobbyStatus.CREATED ? "created" : "running"; String statusStr = status == LobbyStatus.CREATED ? "created" : "running";
return String.format(BUTTON_IMAGE_TEMPLATE, buttonId, statusStr); return String.format(BUTTON_IMAGE_TEMPLATE, lobbyId, statusStr);
} }
private Image safeLoadImage(String path) { private Image safeLoadImage(String path) {
@@ -315,22 +438,20 @@ public class LobbyButtonGridManager {
executor) executor)
.thenAccept( .thenAccept(
status -> { status -> {
String path = getImagePathForButton(buttonId, status); String path = getImagePathForLobby(lobbyId, status);
javafx.application.Platform.runLater( javafx.application.Platform.runLater(
() -> { () -> {
for (Node node : gridPane.getChildren()) { for (Node node : gridPane.getChildren()) {
boolean isButton = node instanceof Button; boolean isButton = node instanceof Button;
boolean idMatches = boolean idMatches = ("lobbyBtn-" + buttonId)
("lobbyBtn-" + buttonId)
.equals(node.getId()); .equals(node.getId());
if (isButton && idMatches) { if (isButton && idMatches) {
Button btn = (Button) node; Button btn = (Button) node;
ImageView iv = ImageView iv = new ImageView(safeLoadImage(path));
new ImageView(safeLoadImage(path));
iv.setPreserveRatio(true); iv.setPreserveRatio(true);
iv.fitWidthProperty() iv.fitWidthProperty()
@@ -375,8 +496,7 @@ public class LobbyButtonGridManager {
javafx.application.Platform.runLater( javafx.application.Platform.runLater(
() -> { () -> {
javafx.stage.Stage currentStage = javafx.stage.Stage currentStage = (javafx.stage.Stage) gridPane.getScene().getWindow();
(javafx.stage.Stage) gridPane.getScene().getWindow();
currentStage.hide(); currentStage.hide();
@@ -54,7 +54,7 @@ public class ServerApp {
private static final int USER_CLEANUP_JOB_RECONNECT_THRESHOLD = 10; private static final int USER_CLEANUP_JOB_RECONNECT_THRESHOLD = 10;
private static final int SESSION_DISCONNECT_JOB_DELAY = 0; private static final int SESSION_DISCONNECT_JOB_DELAY = 0;
private static final int SESSION_DISCONNECT_JOB_PERIOD = 2; private static final int SESSION_DISCONNECT_JOB_PERIOD = 2;
private static final int SESSION_DISCONNECT_JOB_TIMEOUT = 5; private static final int SESSION_DISCONNECT_JOB_TIMEOUT = 30000;
private static final int LOBBY_EXPIRY_SECONDS = 30; private static final int LOBBY_EXPIRY_SECONDS = 30;
private static final int LOBBY_CLEANUP_INITIAL_DELAY_SECONDS = 5; private static final int LOBBY_CLEANUP_INITIAL_DELAY_SECONDS = 5;
private static final int LOBBY_CLEANUP_PERIOD_SECONDS = 5; private static final int LOBBY_CLEANUP_PERIOD_SECONDS = 5;
@@ -94,7 +94,7 @@ public class ServerApp {
TimeUnit.SECONDS); TimeUnit.SECONDS);
LobbyManager lobbyManager = new LobbyManager(); LobbyManager lobbyManager = new LobbyManager();
registerCommands(dispatcher, router, responseDispatcher, userRegistry, lobbyManager); registerCommands(dispatcher, router, responseDispatcher, userRegistry, lobbyManager, sessionManager);
// Periodic cleanup: remove empty lobbies older than 30s and notify affected // Periodic cleanup: remove empty lobbies older than 30s and notify affected
// users // users
@@ -147,7 +147,8 @@ public class ServerApp {
CommandRouter commandRouter, CommandRouter commandRouter,
ResponseDispatcher responseDispatcher, ResponseDispatcher responseDispatcher,
UserRegistry userRegistry, UserRegistry userRegistry,
LobbyManager lobbyManager) { LobbyManager lobbyManager,
SessionManager sessionManager) {
parserDispatcher.register("PING", new PingParser()); parserDispatcher.register("PING", new PingParser());
commandRouter.register(PingRequest.class, new PingHandler(responseDispatcher)); commandRouter.register(PingRequest.class, new PingHandler(responseDispatcher));
@@ -293,7 +294,7 @@ public class ServerApp {
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby
.create_lobby.CreateLobbyRequest>) .create_lobby.CreateLobbyRequest>)
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby
.CreateLobbyHandler(responseDispatcher, lobbyManager)); .CreateLobbyHandler(responseDispatcher, lobbyManager, sessionManager));
// JOIN_LOBBY registration // JOIN_LOBBY registration
parserDispatcher.register( parserDispatcher.register(
@@ -4,14 +4,22 @@ 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.lobby.LobbyManager;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler; 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.ErrorResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.Session;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
public class CreateLobbyHandler extends CommandHandler<CreateLobbyRequest> { public class CreateLobbyHandler extends CommandHandler<CreateLobbyRequest> {
private final LobbyManager lobbyManager; private final LobbyManager lobbyManager;
private final SessionManager sessionManager;
public CreateLobbyHandler(ResponseDispatcher responseDispatcher, LobbyManager lobbyManager) { public CreateLobbyHandler(
ResponseDispatcher responseDispatcher, LobbyManager lobbyManager, SessionManager sessionManager) {
super(responseDispatcher); super(responseDispatcher);
this.lobbyManager = lobbyManager; this.lobbyManager = lobbyManager;
this.sessionManager = sessionManager;
} }
@Override @Override
@@ -28,5 +36,19 @@ public class CreateLobbyHandler extends CommandHandler<CreateLobbyRequest> {
} }
responseDispatcher.dispatch(new CreateLobbyResponse(request.getContext(), id.value())); responseDispatcher.dispatch(new CreateLobbyResponse(request.getContext(), id.value()));
// broadcast LOBBY_CREATED event to all connected sessions (requestId=0)
for (Session s : sessionManager.getAllSessions()) {
RequestContext ctx = new RequestContext(s.getId(), 0);
SuccessResponse ev =
new SuccessResponse(
ctx,
ResponseBody.builder()
.param("EVENT", "LOBBY_CREATED")
.param("LOBBY_ID", id.value())
.build()) {};
responseDispatcher.dispatch(ev);
}
} }
} }