Feat/ms 4 integration #282
@@ -10,10 +10,11 @@ import org.apache.logging.log4j.Logger;
|
|||||||
/**
|
/**
|
||||||
* Entry point and bootstrap helper for the Casono client application.
|
* Entry point and bootstrap helper for the Casono client application.
|
||||||
*
|
*
|
||||||
* Responsibilities:
|
* <p>
|
||||||
* - Parse CLI args (host:port, optional username)
|
* Responsibilities: - Parse CLI args (host:port, optional username) - Store
|
||||||
* - Store username centrally so UI can reuse it
|
* username centrally
|
||||||
* - Create a shared ClientService and do startup LOGIN (optional)
|
* so UI can reuse it - Create a shared ClientService and do startup LOGIN
|
||||||
|
* (optional)
|
||||||
*/
|
*/
|
||||||
public class ClientApp {
|
public class ClientApp {
|
||||||
|
|
||||||
@@ -47,72 +48,83 @@ public class ClientApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void start(String arg, String username) {
|
public static void start(String arg, String username) {
|
||||||
|
String[] hostPort = parseHostAndPort(arg);
|
||||||
|
String host = hostPort[0];
|
||||||
|
int port = Integer.parseInt(hostPort[1]);
|
||||||
|
|
||||||
|
configureServerProperties(host, port);
|
||||||
|
setSharedUsername(username != null && !username.isBlank() ? username.trim() : null);
|
||||||
|
|
||||||
|
if (getSharedUsername() != null) {
|
||||||
|
performStartupLogin(host, port, username);
|
||||||
|
}
|
||||||
|
|
||||||
|
launchUI(arg, username);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String[] parseHostAndPort(String arg) {
|
||||||
String[] parts = arg.split(":", 2);
|
String[] parts = arg.split(":", 2);
|
||||||
if (parts.length != 2) {
|
if (parts.length != 2) {
|
||||||
throw new IllegalArgumentException("The address must be in the format <ip>:<port>.");
|
throw new IllegalArgumentException("The address must be in the format <ip>:<port>.");
|
||||||
}
|
}
|
||||||
String host = parts[0];
|
return parts;
|
||||||
int port = Integer.parseInt(parts[1]);
|
}
|
||||||
|
|
||||||
|
private static void configureServerProperties(String host, int port) {
|
||||||
LOGGER.info("You've selected the client. It will connect port {} at host {}", port, host);
|
LOGGER.info("You've selected the client. It will connect port {} at host {}", port, host);
|
||||||
|
|
||||||
System.setProperty("casono.server.host", host);
|
System.setProperty("casono.server.host", host);
|
||||||
System.setProperty("casono.server.port", String.valueOf(port));
|
System.setProperty("casono.server.port", String.valueOf(port));
|
||||||
|
}
|
||||||
|
|
||||||
if (username != null && !username.isBlank()) {
|
private static void performStartupLogin(String host, int port, String username) {
|
||||||
setSharedUsername(username.trim());
|
try {
|
||||||
} else {
|
ClientService clientService = new ClientService(host, port);
|
||||||
setSharedUsername(null);
|
setSharedClientService(clientService);
|
||||||
}
|
|
||||||
|
|
||||||
if (username != null && !username.isBlank()) {
|
java.util.concurrent.ExecutorService bg = java.util.concurrent.Executors.newSingleThreadExecutor(
|
||||||
try {
|
r -> {
|
||||||
ClientService clientService = new ClientService(host, port);
|
Thread t = new Thread(r);
|
||||||
setSharedClientService(clientService);
|
t.setDaemon(true);
|
||||||
|
t.setName("casono-startup-login");
|
||||||
|
return t;
|
||||||
|
});
|
||||||
|
|
||||||
java.util.concurrent.ExecutorService bg =
|
bg.submit(
|
||||||
java.util.concurrent.Executors.newSingleThreadExecutor(
|
() -> {
|
||||||
r -> {
|
try {
|
||||||
Thread t = new Thread(r);
|
LobbyClient lobbyClient = new LobbyClient(clientService);
|
||||||
t.setDaemon(true);
|
LoginResult res = lobbyClient.login(username);
|
||||||
t.setName("casono-startup-login");
|
|
||||||
return t;
|
|
||||||
});
|
|
||||||
|
|
||||||
bg.submit(
|
if (res != null
|
||||||
() -> {
|
&& res.getUsername() != null
|
||||||
try {
|
&& !res.getUsername().isBlank()) {
|
||||||
LobbyClient lobbyClient = new LobbyClient(clientService);
|
setSharedUsername(res.getUsername().trim());
|
||||||
LoginResult res = lobbyClient.login(username);
|
|
||||||
|
|
||||||
if (res != null && res.getUsername() != null && !res.getUsername().isBlank()) {
|
|
||||||
setSharedUsername(res.getUsername().trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGGER.info(
|
|
||||||
"Assigned username='{}' id={}",
|
|
||||||
res != null ? res.getUsername() : null,
|
|
||||||
res != null ? res.getId() : null);
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
LOGGER.warn("Startup login failed: {}", e.getMessage());
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOGGER.warn("Unexpected error during startup login", e);
|
|
||||||
} finally {
|
|
||||||
bg.shutdown();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
LOGGER.warn(
|
|
||||||
"Could not establish initial connection for startup login: {}",
|
|
||||||
e.getMessage());
|
|
||||||
// UI will create its own connection when it initializes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
LOGGER.info(
|
||||||
|
"Assigned username='{}' id={}",
|
||||||
|
res != null ? res.getUsername() : null,
|
||||||
|
res != null ? res.getId() : null);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
LOGGER.warn("Startup login failed: {}", e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("Unexpected error during startup login", e);
|
||||||
|
} finally {
|
||||||
|
bg.shutdown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
LOGGER.warn(
|
||||||
|
"Could not establish initial connection for startup login: {}",
|
||||||
|
e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void launchUI(String arg, String username) {
|
||||||
if (username != null && !username.isBlank()) {
|
if (username != null && !username.isBlank()) {
|
||||||
Launcher.main(new String[] {arg, username});
|
Launcher.main(new String[] { arg, username });
|
||||||
} else {
|
} else {
|
||||||
Launcher.main(new String[] {arg});
|
Launcher.main(new String[] { arg });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,8 +133,7 @@ public class ClientApp {
|
|||||||
throw new IllegalArgumentException("Address argument required: <host:port>");
|
throw new IllegalArgumentException("Address argument required: <host:port>");
|
||||||
}
|
}
|
||||||
|
|
||||||
String username =
|
String username = args.length > 1 && args[1] != null && !args[1].isBlank() ? args[1] : null;
|
||||||
args.length > 1 && args[1] != null && !args[1].isBlank() ? args[1] : null;
|
|
||||||
|
|
||||||
start(args[0], username);
|
start(args[0], username);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
|
|||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Deque;
|
import java.util.Deque;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -26,8 +26,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 {
|
||||||
@@ -41,20 +43,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) {
|
||||||
@@ -79,25 +81,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();
|
||||||
}
|
}
|
||||||
@@ -107,11 +108,22 @@ public class ClientService {
|
|||||||
String responseText = rp.payload();
|
String responseText = rp.payload();
|
||||||
logger.info("Raw message '{}'", responseText);
|
logger.info("Raw message '{}'", responseText);
|
||||||
|
|
||||||
|
ParsedPacket parsed = parsePacketContent(responseText);
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"Parsed response lines (rid={}, success={}, openBlocks={}): {}",
|
||||||
|
rid,
|
||||||
|
parsed.success,
|
||||||
|
parsed.blockStack.size(),
|
||||||
|
parsed.lines);
|
||||||
|
|
||||||
|
handleParsedResponse(rid, parsed.success, parsed.lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ParsedPacket parsePacketContent(String responseText) {
|
||||||
boolean hasStatus = false;
|
boolean hasStatus = false;
|
||||||
boolean success = false;
|
boolean success = false;
|
||||||
|
|
||||||
List<String> lines = new ArrayList<>();
|
List<String> lines = new ArrayList<>();
|
||||||
|
|
||||||
Deque<String> blockStack = new ArrayDeque<>();
|
Deque<String> blockStack = new ArrayDeque<>();
|
||||||
|
|
||||||
for (String rawLine : responseText.split("\\n")) {
|
for (String rawLine : responseText.split("\\n")) {
|
||||||
@@ -119,20 +131,16 @@ public class ClientService {
|
|||||||
String trimmed = line.trim();
|
String trimmed = line.trim();
|
||||||
|
|
||||||
if (!hasStatus) {
|
if (!hasStatus) {
|
||||||
if ("+OK".equals(trimmed)) {
|
StatusCheckResult status = checkStatus(trimmed);
|
||||||
success = true;
|
if (status != null) {
|
||||||
|
success = status.success;
|
||||||
hasStatus = true;
|
hasStatus = true;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (trimmed.startsWith("-ERR") || trimmed.startsWith("-ERROR")) {
|
|
||||||
success = false;
|
|
||||||
hasStatus = true;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trimmed.isEmpty()) continue;
|
if (trimmed.isEmpty())
|
||||||
|
continue;
|
||||||
|
|
||||||
line = line.replaceFirst("^\\s+", "");
|
line = line.replaceFirst("^\\s+", "");
|
||||||
trimmed = line.trim();
|
trimmed = line.trim();
|
||||||
@@ -149,7 +157,6 @@ public class ClientService {
|
|||||||
lines.add("END");
|
lines.add("END");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,21 +164,34 @@ public class ClientService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!hasStatus) {
|
if (!hasStatus) {
|
||||||
if (responseText.contains("+OK")) {
|
success = inferStatus(responseText);
|
||||||
success = true;
|
|
||||||
hasStatus = true;
|
|
||||||
} else if (responseText.contains("-ERR") || responseText.contains("-ERROR")) {
|
|
||||||
success = false;
|
|
||||||
hasStatus = true;
|
|
||||||
} else {
|
|
||||||
success = true;
|
|
||||||
hasStatus = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("Parsed response lines (rid={}, success={}, openBlocks={}): {}",
|
return new ParsedPacket(success, lines, blockStack);
|
||||||
rid, success, blockStack.size(), lines);
|
}
|
||||||
|
|
||||||
|
private StatusCheckResult checkStatus(String trimmed) {
|
||||||
|
if ("+OK".equals(trimmed)) {
|
||||||
|
return new StatusCheckResult(true);
|
||||||
|
}
|
||||||
|
if (trimmed.startsWith("-ERR") || trimmed.startsWith("-ERROR")) {
|
||||||
|
return new StatusCheckResult(false);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean inferStatus(String responseText) {
|
||||||
|
if (responseText.contains("+OK")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (responseText.contains("-ERR") || responseText.contains("-ERROR")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleParsedResponse(int rid, boolean success, List<String> lines)
|
||||||
|
throws InterruptedException {
|
||||||
if (rid == 0) {
|
if (rid == 0) {
|
||||||
for (Consumer<List<String>> l : eventListeners) {
|
for (Consumer<List<String>> l : eventListeners) {
|
||||||
try {
|
try {
|
||||||
@@ -190,6 +210,12 @@ public class ClientService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record ParsedPacket(boolean success, List<String> lines, Deque<String> blockStack) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record StatusCheckResult(boolean success) {
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isContainerStart(String token) {
|
private boolean isContainerStart(String token) {
|
||||||
return token.equals("LOBBIES")
|
return token.equals("LOBBIES")
|
||||||
|| token.equals("LOBBY")
|
|| token.equals("LOBBY")
|
||||||
@@ -202,7 +228,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
|
||||||
@@ -215,19 +242,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.
|
||||||
@@ -238,8 +267,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.
|
||||||
@@ -264,10 +295,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);
|
||||||
@@ -278,14 +311,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) {
|
||||||
@@ -296,15 +333,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();
|
||||||
@@ -336,11 +372,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);
|
||||||
@@ -354,9 +394,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.
|
||||||
@@ -374,8 +417,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() {
|
||||||
|
|||||||
@@ -13,17 +13,21 @@ import java.util.logging.Logger;
|
|||||||
/**
|
/**
|
||||||
* Fetches and parses game state from server.
|
* Fetches and parses game state from server.
|
||||||
*
|
*
|
||||||
* Protocol notes (based on your current server implementation):
|
* <p>
|
||||||
* - Success replies contain: PHASE, POT, CURRENT_BET, DEALER, ACTIVE_PLAYER, then blocks:
|
* Protocol notes (based on your current server implementation): - Success
|
||||||
* - CARD ... END (community cards on root level)
|
* replies contain:
|
||||||
* - PLAYER ... (NAME/CHIPS/BET/STATE + optional CARD blocks for requesting player) ... END
|
* PHASE, POT, CURRENT_BET, DEALER, ACTIVE_PLAYER, then blocks: - CARD ... END
|
||||||
* - Error replies contain: -ERR then CODE=..., MSG=..., END
|
* (community cards on
|
||||||
|
* root level) - PLAYER ... (NAME/CHIPS/BET/STATE + optional CARD blocks for
|
||||||
|
* requesting player) ...
|
||||||
|
* END - Error replies contain: -ERR then CODE=..., MSG=..., END
|
||||||
*
|
*
|
||||||
* Important:
|
* <p>
|
||||||
* - The server does NOT wrap cards in a "CARDS" container.
|
* Important: - The server does NOT wrap cards in a "CARDS" container. - CARD
|
||||||
* - CARD blocks appear either:
|
* blocks appear
|
||||||
* - at root level -> community cards
|
* either: - at root level -> community cards - inside PLAYER -> hole cards for
|
||||||
* - inside PLAYER -> hole cards for that player (usually only for the requesting user)
|
* that player (usually
|
||||||
|
* only for the requesting user)
|
||||||
*/
|
*/
|
||||||
public class GameClient {
|
public class GameClient {
|
||||||
|
|
||||||
@@ -33,7 +37,8 @@ public class GameClient {
|
|||||||
private final int gameId;
|
private final int gameId;
|
||||||
|
|
||||||
public GameClient(ClientService client, int gameId) {
|
public GameClient(ClientService client, int gameId) {
|
||||||
if (client == null) throw new IllegalArgumentException("ClientService must not be null");
|
if (client == null)
|
||||||
|
throw new IllegalArgumentException("ClientService must not be null");
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.gameId = gameId;
|
this.gameId = gameId;
|
||||||
LOG.info(() -> "GameClient initialized for gameId=" + gameId);
|
LOG.info(() -> "GameClient initialized for gameId=" + gameId);
|
||||||
@@ -72,10 +77,17 @@ public class GameClient {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
GameState state = parseGameState(joined);
|
GameState state = parseGameState(joined);
|
||||||
LOG.info(() -> "Parsed state: phase=" + state.phase
|
LOG.info(
|
||||||
+ " pot=" + state.pot
|
() -> "Parsed state: phase="
|
||||||
+ " players=" + (state.players != null ? state.players.size() : 0)
|
+ state.phase
|
||||||
+ " community=" + (state.communityCards != null ? state.communityCards.size() : 0));
|
+ " pot="
|
||||||
|
+ state.pot
|
||||||
|
+ " players="
|
||||||
|
+ (state.players != null ? state.players.size() : 0)
|
||||||
|
+ " community="
|
||||||
|
+ (state.communityCards != null
|
||||||
|
? state.communityCards.size()
|
||||||
|
: 0));
|
||||||
return state;
|
return state;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.log(Level.SEVERE, "Failed parsing game state. Payload=\n" + joined, e);
|
LOG.log(Level.SEVERE, "Failed parsing game state. Payload=\n" + joined, e);
|
||||||
@@ -101,115 +113,128 @@ public class GameClient {
|
|||||||
|
|
||||||
private GameState parseGameState(String input) {
|
private GameState parseGameState(String input) {
|
||||||
GameState s = new GameState();
|
GameState s = new GameState();
|
||||||
|
s.players = new ArrayList<>();
|
||||||
|
s.communityCards = new ArrayList<>();
|
||||||
|
|
||||||
if (s.players == null) s.players = new ArrayList<>();
|
ParseState state = new ParseState();
|
||||||
if (s.communityCards == null) s.communityCards = new ArrayList<>();
|
|
||||||
|
|
||||||
Player currentPlayer = null;
|
|
||||||
Card currentCard = null;
|
|
||||||
boolean insidePlayer = false;
|
|
||||||
|
|
||||||
for (String raw : input.split("\n")) {
|
for (String raw : input.split("\n")) {
|
||||||
String line = raw.trim();
|
String line = raw.trim();
|
||||||
if (line.isEmpty()) continue;
|
if (line.isEmpty() || line.startsWith("+OK"))
|
||||||
if (line.startsWith("+OK")) continue;
|
continue;
|
||||||
|
|
||||||
// Global fields
|
if (!parseGlobalField(s, line)
|
||||||
if (line.startsWith("PHASE=")) {
|
&& !parsePlayerStructure(s, state, line)
|
||||||
s.phase = value(line);
|
&& !parseCardData(state, line)
|
||||||
continue;
|
&& !parsePlayerData(state, line)) {
|
||||||
|
LOG.fine(() -> "Ignored line: '" + line + "'");
|
||||||
}
|
}
|
||||||
if (line.startsWith("POT=")) {
|
|
||||||
s.pot = intVal(line);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (line.startsWith("CURRENT_BET=")) {
|
|
||||||
s.currentBet = intVal(line);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (line.startsWith("DEALER=")) {
|
|
||||||
s.dealer = intVal(line);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (line.startsWith("ACTIVE_PLAYER=")) {
|
|
||||||
s.activePlayer = intVal(line);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (line.startsWith("WINNER=")) {
|
|
||||||
s.winnerIndex = intVal(line);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line.equals("PLAYER")) {
|
|
||||||
currentPlayer = new Player();
|
|
||||||
s.players.add(currentPlayer);
|
|
||||||
currentCard = null;
|
|
||||||
insidePlayer = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line.equals("CARDS")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line.equals("CARD")) {
|
|
||||||
currentCard = new Card("", "");
|
|
||||||
|
|
||||||
if (insidePlayer && currentPlayer != null) {
|
|
||||||
currentPlayer.addCard(currentCard); // hole card
|
|
||||||
} else {
|
|
||||||
s.communityCards.add(currentCard); // community card
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line.startsWith("VALUE=") && currentCard != null) {
|
|
||||||
currentCard.setValue(value(line));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (line.startsWith("SUIT=") && currentCard != null) {
|
|
||||||
currentCard.setSuit(value(line));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line.equals("END")) {
|
|
||||||
if (currentCard != null) { // schließt CARD
|
|
||||||
currentCard = null;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (insidePlayer) { // schließt PLAYER
|
|
||||||
insidePlayer = false;
|
|
||||||
currentPlayer = null;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
continue; // root END
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentPlayer != null) {
|
|
||||||
if (line.startsWith("USERNAME=") || line.startsWith("NAME=")) {
|
|
||||||
currentPlayer.setId(PlayerId.of(value(line)));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (line.startsWith("CHIPS=")) {
|
|
||||||
currentPlayer.setChips(intVal(line));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (line.startsWith("BET=")) {
|
|
||||||
currentPlayer.setBet(intVal(line));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (line.startsWith("STATE=")) {
|
|
||||||
currentPlayer.setState(parseState(value(line)));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG.fine(() -> "Ignored line: '" + line + "'");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean parseGlobalField(GameState s, String line) {
|
||||||
|
return switch (line) {
|
||||||
|
case String l when l.startsWith("PHASE=") -> {
|
||||||
|
s.phase = value(l);
|
||||||
|
yield true;
|
||||||
|
}
|
||||||
|
case String l when l.startsWith("POT=") -> {
|
||||||
|
s.pot = intVal(l);
|
||||||
|
yield true;
|
||||||
|
}
|
||||||
|
case String l when l.startsWith("CURRENT_BET=") -> {
|
||||||
|
s.currentBet = intVal(l);
|
||||||
|
yield true;
|
||||||
|
}
|
||||||
|
case String l when l.startsWith("DEALER=") -> {
|
||||||
|
s.dealer = intVal(l);
|
||||||
|
yield true;
|
||||||
|
}
|
||||||
|
case String l when l.startsWith("ACTIVE_PLAYER=") -> {
|
||||||
|
s.activePlayer = intVal(l);
|
||||||
|
yield true;
|
||||||
|
}
|
||||||
|
case String l when l.startsWith("WINNER=") -> {
|
||||||
|
s.winnerIndex = intVal(l);
|
||||||
|
yield true;
|
||||||
|
}
|
||||||
|
default -> false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean parsePlayerStructure(GameState s, ParseState state, String line) {
|
||||||
|
if (line.equals("PLAYER")) {
|
||||||
|
state.currentPlayer = new Player();
|
||||||
|
s.players.add(state.currentPlayer);
|
||||||
|
state.currentCard = null;
|
||||||
|
state.insidePlayer = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (line.equals("CARDS"))
|
||||||
|
return true;
|
||||||
|
if (line.equals("CARD")) {
|
||||||
|
state.currentCard = new Card("", "");
|
||||||
|
if (state.insidePlayer && state.currentPlayer != null) {
|
||||||
|
state.currentPlayer.addCard(state.currentCard);
|
||||||
|
} else {
|
||||||
|
s.communityCards.add(state.currentCard);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (line.equals("END")) {
|
||||||
|
if (state.currentCard != null) {
|
||||||
|
state.currentCard = null;
|
||||||
|
} else if (state.insidePlayer) {
|
||||||
|
state.insidePlayer = false;
|
||||||
|
state.currentPlayer = null;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean parseCardData(ParseState state, String line) {
|
||||||
|
if (state.currentCard == null)
|
||||||
|
return false;
|
||||||
|
if (line.startsWith("VALUE=")) {
|
||||||
|
state.currentCard.setValue(value(line));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (line.startsWith("SUIT=")) {
|
||||||
|
state.currentCard.setSuit(value(line));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean parsePlayerData(ParseState state, String line) {
|
||||||
|
if (state.currentPlayer == null)
|
||||||
|
return false;
|
||||||
|
if (line.startsWith("USERNAME=") || line.startsWith("NAME=")) {
|
||||||
|
state.currentPlayer.setId(PlayerId.of(value(line)));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (line.startsWith("CHIPS=")) {
|
||||||
|
state.currentPlayer.setChips(intVal(line));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (line.startsWith("BET=")) {
|
||||||
|
state.currentPlayer.setBet(intVal(line));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (line.startsWith("STATE=")) {
|
||||||
|
state.currentPlayer.setState(parseState(value(line)));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ParseState {
|
||||||
|
Player currentPlayer;
|
||||||
|
Card currentCard;
|
||||||
|
boolean insidePlayer;
|
||||||
|
}
|
||||||
|
|
||||||
private static String value(String line) {
|
private static String value(String line) {
|
||||||
return line.split("=", 2)[1];
|
return line.split("=", 2)[1];
|
||||||
}
|
}
|
||||||
@@ -219,7 +244,8 @@ public class GameClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static PlayerState parseState(String s) {
|
private static PlayerState parseState(String s) {
|
||||||
if (s == null) return PlayerState.ACTIVE;
|
if (s == null)
|
||||||
|
return PlayerState.ACTIVE;
|
||||||
return switch (s.trim().toUpperCase()) {
|
return switch (s.trim().toUpperCase()) {
|
||||||
case "ACTIVE" -> PlayerState.ACTIVE;
|
case "ACTIVE" -> PlayerState.ACTIVE;
|
||||||
case "FOLDED" -> PlayerState.FOLDED;
|
case "FOLDED" -> PlayerState.FOLDED;
|
||||||
@@ -229,7 +255,8 @@ public class GameClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String extractValue(String joined, String key) {
|
private static String extractValue(String joined, String key) {
|
||||||
if (joined == null) return null;
|
if (joined == null)
|
||||||
|
return null;
|
||||||
for (String raw : joined.split("\n")) {
|
for (String raw : joined.split("\n")) {
|
||||||
String line = raw.trim();
|
String line = raw.trim();
|
||||||
if (line.startsWith(key + "=")) {
|
if (line.startsWith(key + "=")) {
|
||||||
|
|||||||
Reference in New Issue
Block a user