From d855f2d5685811a471180e0f12996e934b7b1cac Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Wed, 8 Apr 2026 13:29:39 +0200 Subject: [PATCH] Feat: add offline mode to prevent errors when trying to send requests to not connected server --- .../casono/client/network/ClientService.java | 34 ++++++- .../ui/lobbyui/CasinomainuiController.java | 92 +++++++++++++++++-- 2 files changed, 116 insertions(+), 10 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java index c4d8550..e68535b 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java @@ -26,6 +26,7 @@ public class ClientService { private final Socket socket; private final ExecutorService executor; + private final boolean offlineMode; public static ArrayList response; private final AtomicInteger idGenerator; @@ -42,6 +43,7 @@ public class ClientService { this.idGenerator = new AtomicInteger(0); + this.offlineMode = false; try { socket = new Socket(ip, port); clienttcptransport = new TcpTransport(socket); @@ -52,6 +54,27 @@ public class ClientService { executor = Executors.newSingleThreadExecutor(); } + /** + * Constructs a ClientService in offline mode. No network connection will be + * attempted and calls to processCommand will throw a RuntimeException. + * + * @param offline true to create an offline (no-network) client service + */ + public ClientService(boolean offline) { + this.idGenerator = new AtomicInteger(0); + this.offlineMode = offline; + this.socket = null; + this.clienttcptransport = null; + this.executor = Executors.newSingleThreadExecutor(); + } + + /** + * Returns true if this ClientService is running in offline mode (no network). + */ + public boolean isOffline() { + return offlineMode; + } + /** * Sends a command to the server and waits for the response. The command is * sent using the TcpTransport, and the response is read in a loop until a @@ -62,6 +85,9 @@ public class ClientService { * @return The response from the server as a string. */ protected String processCommand(String message) { + if (offlineMode) { + throw new RuntimeException("ClientService is offline: cannot process command"); + } AtomicReference response = new AtomicReference<>(); sendRequest(() -> { try { @@ -135,8 +161,12 @@ public class ClientService { public void closeSocket() { try { executor.shutdown(); - clienttcptransport.close(); - socket.close(); + if (clienttcptransport != null) { + clienttcptransport.close(); + } + if (socket != null) { + socket.close(); + } } catch (IOException j) { System.out.println(j); } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/CasinomainuiController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/CasinomainuiController.java index 565804b..06414cb 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/CasinomainuiController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/CasinomainuiController.java @@ -3,6 +3,9 @@ package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui; import javafx.application.Platform; import javafx.fxml.FXML; import javafx.scene.control.Button; +import javafx.scene.control.TextField; +import javafx.scene.control.Alert; +import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Label; import javafx.scene.image.Image; import javafx.scene.image.ImageView; @@ -12,6 +15,7 @@ import javafx.scene.shape.Rectangle; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService; +import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient; /** * Controller for the Casono main UI lobby. Handles UI initialization and user @@ -20,17 +24,29 @@ import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService; public class CasinomainuiController { private static final Logger LOGGER = LogManager.getLogger(CasinomainuiController.class); - @FXML private AnchorPane rootPane; - @FXML private Label titleLabel; - @FXML private Label subtitleLabel; - @FXML private ImageView logoView; - @FXML private Rectangle greenBox; - @FXML private Button exitbutton; - @FXML private VBox casinoTable; + @FXML + private AnchorPane rootPane; + @FXML + private Label titleLabel; + @FXML + private Label subtitleLabel; + @FXML + private ImageView logoView; + @FXML + private Rectangle greenBox; + @FXML + private Button exitbutton; + @FXML + private VBox casinoTable; + @FXML + private TextField usernameField; + @FXML + private Button loginButton; private LobbyButtonTranslationManager translationManager; private LobbyButtonGridManager gridManager; private int nextButtonId = 1; + private LobbyClient lobbyClient; /** Default constructor for dependency injection by FXMLLoader. */ public CasinomainuiController() { @@ -47,8 +63,60 @@ public class CasinomainuiController { translationManager = LobbyButtonTranslationManager.getInstance(); String host = System.getProperty("casono.server.host"); int port = Integer.parseInt(System.getProperty("casono.server.port")); - ClientService clientService = new ClientService(host, port); + ClientService clientService; + try { + clientService = new ClientService(host, port); + } catch (RuntimeException e) { + LOGGER.warn("Could not connect to server {}:{} — starting in offline mode: {}", host, port, e.getMessage()); + clientService = new ClientService(true); // offline mode + } gridManager = new LobbyButtonGridManager(new javafx.scene.layout.GridPane(), translationManager, clientService); + // LobbyClient will use the provided ClientService; in offline mode calls will + // fail with RuntimeException + lobbyClient = new LobbyClient(clientService); + casinoTable.getChildren().clear(); + casinoTable.getChildren().add(gridManager.getGridPane()); + gridManager.renderLobbyButtons(); + } + + /** + * Handles the login button action. Validates input and calls + * LobbyClient.login(). + */ + @FXML + public void handleLoginButton() { + String username = usernameField.getText(); + if (username == null || username.isBlank()) { + showAlert("Please enter a username."); + return; + } + // Only allow alphanumeric, _ and - + if (!username.matches("[a-zA-Z0-9_-]+")) { + showAlert("Only letters, numbers, '_' and '-' are allowed!"); + return; + } + if (lobbyClient.getClientService().isOffline()) { + showAlert("Offline mode: cannot send login to server."); + return; + } + try { + lobbyClient.login(username); + showAlert("Login sent: " + username); + } catch (RuntimeException e) { + LOGGER.error("Login failed: {}", e.getMessage()); + showAlert("Login failed: " + e.getMessage()); + } + } + + /** + * Shows an alert dialog with the given message. + */ + private void showAlert(String message) { + Alert alert = new Alert(AlertType.INFORMATION); + alert.setTitle("Info"); + alert.setHeaderText(null); + alert.setContentText(message); + alert.showAndWait(); casinoTable.getChildren().clear(); casinoTable.getChildren().add(gridManager.getGridPane()); gridManager.renderLobbyButtons(); @@ -69,6 +137,14 @@ public class CasinomainuiController { } int buttonId = nextButtonId++; try { + String username = usernameField != null ? usernameField.getText() : ""; + LOGGER.info("Creating lobby for user: {}", username); + // avoid attempting to create a lobby when offline + if (lobbyClient.getClientService().isOffline()) { + LOGGER.warn("Cannot create lobby while offline"); + showAlert("Offline mode: cannot create lobby."); + return; + } int lobbyId = gridManager.createLobby(); translationManager.addLobbyButton(buttonId, lobbyId); LOGGER.info("ButtonID: {}, LobbyID: {}", buttonId, lobbyId);