Feat/ms 4 integration #282
@@ -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,20 +41,20 @@ 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.
|
||||||
* @param port The port number of the server to connect to.
|
* @param port The port number of the server to connect to.
|
||||||
*/
|
*/
|
||||||
public ClientService(String ip, int port) {
|
public ClientService(String ip, int port) {
|
||||||
@@ -77,25 +79,24 @@ 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 {
|
RawPacket rp = clienttcptransport.read();
|
||||||
RawPacket rp = clienttcptransport.read();
|
processRawPacket(rp);
|
||||||
processRawPacket(rp);
|
} catch (IOException e) {
|
||||||
} catch (IOException e) {
|
if (running.get()) {
|
||||||
if (running.get()) {
|
logger.warn("IO error on transport reader", e);
|
||||||
logger.warn("IO error on transport reader", e);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
break;
|
||||||
"casono-client-reader");
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"casono-client-reader");
|
||||||
readerThread.setDaemon(true);
|
readerThread.setDaemon(true);
|
||||||
readerThread.start();
|
readerThread.start();
|
||||||
}
|
}
|
||||||
@@ -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,14 +253,18 @@ 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).
|
||||||
* occurs.
|
* @throws RuntimeException if the server responds with an error or if a
|
||||||
|
* communication failure
|
||||||
|
* occurs.
|
||||||
*/
|
*/
|
||||||
protected List<String> processCommand(String message) {
|
protected List<String> processCommand(String message) {
|
||||||
if (offlineMode) {
|
if (offlineMode) {
|
||||||
@@ -262,15 +275,14 @@ 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));
|
} catch (IOException e) {
|
||||||
} catch (IOException e) {
|
throw new RuntimeException(e);
|
||||||
throw new RuntimeException(e);
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
writeFuture.get();
|
writeFuture.get();
|
||||||
@@ -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() {
|
||||||
|
|||||||
+153
-33
@@ -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,8 +149,29 @@ 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()) {
|
||||||
return;
|
// mapping may still be empty after reconciliation
|
||||||
|
if (translationManager.getButtonIdToLobbyId().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mapping = translationManager.getButtonIdToLobbyId();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Map.Entry<Integer, Integer>> entries = new ArrayList<>(mapping.entrySet());
|
List<Map.Entry<Integer, Integer>> entries = new ArrayList<>(mapping.entrySet());
|
||||||
@@ -125,26 +185,91 @@ public class LobbyButtonGridManager {
|
|||||||
int lobbyId = lobbyIdObj.intValue();
|
int lobbyId = lobbyIdObj.intValue();
|
||||||
|
|
||||||
CompletableFuture.supplyAsync(
|
CompletableFuture.supplyAsync(
|
||||||
() -> {
|
() -> {
|
||||||
try {
|
try {
|
||||||
return lobbyClient.fetchLobbyStatusString(lobbyId);
|
return lobbyClient.fetchLobbyStatusString(lobbyId);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
LOGGER.info("Lobby {} missing: {}", lobbyId, ex.getMessage());
|
LOGGER.info("Lobby {} missing: {}", lobbyId, ex.getMessage());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
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) {
|
||||||
translationManager.removeLobbyButton(buttonId);
|
if (!serverIds.contains(lobbyId)) {
|
||||||
|
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) {
|
||||||
@@ -305,32 +428,30 @@ public class LobbyButtonGridManager {
|
|||||||
int lobbyId = lobbyIdObj.intValue();
|
int lobbyId = lobbyIdObj.intValue();
|
||||||
|
|
||||||
CompletableFuture.supplyAsync(
|
CompletableFuture.supplyAsync(
|
||||||
() -> {
|
() -> {
|
||||||
String statusStr = lobbyClient.fetchLobbyStatusString(lobbyId);
|
String statusStr = lobbyClient.fetchLobbyStatusString(lobbyId);
|
||||||
|
|
||||||
LobbyStatus status = parseLobbyStatus(statusStr);
|
LobbyStatus status = parseLobbyStatus(statusStr);
|
||||||
|
|
||||||
return status == null ? LobbyStatus.CREATED : status;
|
return status == null ? LobbyStatus.CREATED : status;
|
||||||
},
|
},
|
||||||
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));
|
||||||
|
|
||||||
@@ -292,8 +293,8 @@ public class ServerApp {
|
|||||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||||
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(
|
||||||
|
|||||||
+23
-1
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user