From bc552428fa72ed8233cb7454ee2aa591316edd2b Mon Sep 17 00:00:00 2001 From: MostCromulent Date: Thu, 5 Feb 2026 06:20:00 +1030 Subject: [PATCH 1/7] Add UI enhancements for network play Improves user experience during network games: - Waiting timer shows player name and elapsed time (CMatchUI, InputLockUI) - Connection errors display detailed messages instead of generic failure - Adds CONN_ERROR_PREFIX constant for structured error handling - New localization strings for waiting and error messages UI-only changes, no game logic or delta sync functionality. Co-Authored-By: Claude Opus 4.5 --- .../home/online/CSubmenuOnlineLobby.java | 10 +- .../java/forge/screens/match/CMatchUI.java | 176 +++++++++++++++++- .../screens/online/OnlineLobbyScreen.java | 10 +- forge-gui/res/languages/en-US.properties | 9 + .../gamemodes/match/input/InputLockUI.java | 115 +++++++++++- .../forge/gamemodes/net/NetConnectUtil.java | 41 +++- .../properties/ForgeConstants.java | 1 + 7 files changed, 353 insertions(+), 9 deletions(-) diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/online/CSubmenuOnlineLobby.java b/forge-gui-desktop/src/main/java/forge/screens/home/online/CSubmenuOnlineLobby.java index 523b83d0cfe..2fcb1ded4cb 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/online/CSubmenuOnlineLobby.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/online/CSubmenuOnlineLobby.java @@ -86,10 +86,16 @@ private void join(final String url) { }); final ChatMessage result = NetConnectUtil.join(url, VSubmenuOnlineLobby.SINGLETON_INSTANCE, FNetOverlay.SINGLETON_INSTANCE); - if(Objects.equals(result.getMessage(), ForgeConstants.CLOSE_CONN_COMMAND)) { + String message = result.getMessage(); + if(Objects.equals(message, ForgeConstants.CLOSE_CONN_COMMAND)) { FOptionPane.showErrorDialog(Localizer.getInstance().getMessage("UnableConnectToServer", url)); SOverlayUtils.hideOverlay(); - } else if (Objects.equals(result.getMessage(), ForgeConstants.INVALID_HOST_COMMAND)) { + } else if (message != null && message.startsWith(ForgeConstants.CONN_ERROR_PREFIX)) { + // Show detailed connection error + String errorDetail = message.substring(ForgeConstants.CONN_ERROR_PREFIX.length()); + FOptionPane.showErrorDialog(errorDetail, Localizer.getInstance().getMessage("lblConnectionError")); + SOverlayUtils.hideOverlay(); + } else if (Objects.equals(message, ForgeConstants.INVALID_HOST_COMMAND)) { FOptionPane.showErrorDialog(Localizer.getInstance().getMessage("lblDetectedInvalidHostAddress", url)); SOverlayUtils.hideOverlay(); } else { diff --git a/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java b/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java index eb0c753b8be..a6b7c51f698 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java +++ b/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java @@ -176,6 +176,11 @@ public final class CMatchUI private final CStack cStack = new CStack(this); private int nextNotifiableStackIndex = 0; + // Timer for "Waiting for..." messages in network games + private javax.swing.Timer waitingTimer; + private long waitingStartTime; + private String waitingBaseMessage; + public CMatchUI() { this.view = new VMatchUI(this); this.screen = FScreen.getMatchScreen(this, view); @@ -819,6 +824,7 @@ public void enableOverlay() { @Override public void finishGame() { + stopWaitingTimer(); // Clean up waiting timer FloatingZone.closeAll(); //ensure floating card areas cleared and closed after the game final GameView gameView = getGameView(); if (hasLocalPlayers() || gameView.isMatchOver()) { @@ -963,7 +969,175 @@ public void popupMenuCanceled(PopupMenuEvent e) { @Override public void showPromptMessage(final PlayerView playerView, final String message) { - cPrompt.setMessage(message); + // First, enhance generic waiting messages with player name (for host) + String enhancedMessage = enhanceWaitingMessageForHost(message, playerView); + + // Handle timer for "Waiting for..." messages in network games + if (GuiBase.isNetworkplay() && enhancedMessage != null && isWaitingMessage(enhancedMessage)) { + // Check if this is the same base message (just timer update) or a new waiting message + String baseMsg = extractBaseWaitingMessage(enhancedMessage); + if (!baseMsg.equals(waitingBaseMessage)) { + // New waiting message - start timer + stopWaitingTimer(); + waitingBaseMessage = baseMsg; + waitingStartTime = System.currentTimeMillis(); + startWaitingTimer(); + } + // Display message with current timer + cPrompt.setMessage(getWaitingMessageWithTimer()); + } else { + // Not a waiting message - stop timer and show message directly + stopWaitingTimer(); + waitingBaseMessage = null; + cPrompt.setMessage(enhancedMessage); + } + } + + /** + * Enhance generic "Waiting for opponent" and "Yielding" messages with the actual player name. + * This is used on the host side where messages aren't sent through NetGuiGame. + */ + private String enhanceWaitingMessageForHost(final String message, final PlayerView forPlayer) { + if (!GuiBase.isNetworkplay() || message == null || message.isEmpty()) { + return message; + } + + Localizer localizer = Localizer.getInstance(); + String waitingForOpponent = localizer.getMessage("lblWaitingForOpponent"); + String yieldingMessage = localizer.getMessage("lblYieldingUntilEndOfTurn"); + + boolean isWaitingOpponent = message.equals(waitingForOpponent); + boolean isYielding = message.equals(yieldingMessage); + + if (!isWaitingOpponent && !isYielding) { + return message; + } + + // Get the priority player from the Game object + GameView gameView = getGameView(); + if (gameView == null) { + return message; + } + + // Find the player we're waiting for + String waitingForName = findWaitingForPlayerName(gameView, forPlayer); + if (waitingForName == null) { + return message; + } + + // Return enhanced message with player name + if (isYielding) { + return localizer.getMessage("lblYieldingWaitingForPlayer", waitingForName); + } else { + return localizer.getMessage("lblWaitingForPlayer", waitingForName); + } + } + + /** + * Find the name of the player we're waiting for. + * Checks priority player, turn player, or falls back to finding other players. + */ + private String findWaitingForPlayerName(GameView gameView, PlayerView forPlayer) { + // Try to get priority player from Game object (host has access to this) + forge.game.Game game = gameView.getGame(); + if (game != null && !game.isGameOver()) { + forge.game.phase.PhaseHandler ph = game.getPhaseHandler(); + if (ph != null) { + forge.game.player.Player priorityPlayer = ph.getPriorityPlayer(); + if (priorityPlayer == null) { + priorityPlayer = ph.getPlayerTurn(); + } + // During mulligan, both may be null - find other players + if (priorityPlayer == null) { + // If forPlayer is known, find someone else + if (forPlayer != null) { + for (forge.game.player.Player p : game.getPlayers()) { + if (p.getView().getId() != forPlayer.getId()) { + return p.getName(); + } + } + } + // If forPlayer is null, find any non-local player + for (forge.game.player.Player p : game.getPlayers()) { + if (!isLocalPlayer(p.getView())) { + return p.getName(); + } + } + } + if (priorityPlayer != null && (forPlayer == null || priorityPlayer.getView().getId() != forPlayer.getId())) { + return priorityPlayer.getName(); + } + } + } + + // Fallback: use GameView's player list to find non-local players + if (gameView.getPlayers() != null) { + for (PlayerView pv : gameView.getPlayers()) { + if (!isLocalPlayer(pv)) { + return pv.getName(); + } + } + } + + return null; + } + + private boolean isWaitingMessage(String message) { + Localizer localizer = Localizer.getInstance(); + String waitingForOpponent = localizer.getMessage("lblWaitingForOpponent"); + String waitingForActions = localizer.getMessage("lblWaitingforActions"); + // Also match "Waiting for PlayerName..." and "Yielding ... Waiting for PlayerName..." patterns + return message.equals(waitingForOpponent) + || message.equals(waitingForActions) + || message.startsWith("Waiting for ") + || message.contains("Waiting for "); + } + + private String extractBaseWaitingMessage(String message) { + // Remove any existing timer suffix like " (5s)" or " (1:23)" + return message.replaceAll(" \\(\\d+s\\)$", "").replaceAll(" \\(\\d+:\\d{2}\\)$", ""); + } + + private String getWaitingMessageWithTimer() { + if (waitingBaseMessage == null) { + return ""; + } + long elapsedMs = System.currentTimeMillis() - waitingStartTime; + long elapsedSec = elapsedMs / 1000; + + // Don't show timer for very short waits + if (elapsedSec < 2) { + return waitingBaseMessage; + } + + String timeStr; + if (elapsedSec < 60) { + timeStr = elapsedSec + "s"; + } else { + long minutes = elapsedSec / 60; + long seconds = elapsedSec % 60; + timeStr = String.format("%d:%02d", minutes, seconds); + } + return waitingBaseMessage + " (" + timeStr + ")"; + } + + private void startWaitingTimer() { + if (waitingTimer != null) { + return; // Already running + } + waitingTimer = new javax.swing.Timer(1000, e -> { + if (waitingBaseMessage != null) { + cPrompt.setMessage(getWaitingMessageWithTimer()); + } + }); + waitingTimer.start(); + } + + private void stopWaitingTimer() { + if (waitingTimer != null) { + waitingTimer.stop(); + waitingTimer = null; + } } @Override diff --git a/forge-gui-mobile/src/forge/screens/online/OnlineLobbyScreen.java b/forge-gui-mobile/src/forge/screens/online/OnlineLobbyScreen.java index e911d9f135b..de05337d85a 100644 --- a/forge-gui-mobile/src/forge/screens/online/OnlineLobbyScreen.java +++ b/forge-gui-mobile/src/forge/screens/online/OnlineLobbyScreen.java @@ -107,10 +107,16 @@ public void onActivate() { final IOnlineChatInterface chatInterface = (IOnlineChatInterface)OnlineScreen.Chat.getScreen(); if (joinServer) { result[0] = NetConnectUtil.join(url, OnlineLobbyScreen.this, chatInterface); - if (result[0].getMessage() == ForgeConstants.CLOSE_CONN_COMMAND) { //this message is returned via netconnectutil on exception + String message = result[0].getMessage(); + if (ForgeConstants.CLOSE_CONN_COMMAND.equals(message)) { //this message is returned via netconnectutil on exception closeConn(Forge.getLocalizer().getMessage("UnableConnectToServer", url)); return; - } else if (result[0].getMessage() == ForgeConstants.INVALID_HOST_COMMAND) { + } else if (message != null && message.startsWith(ForgeConstants.CONN_ERROR_PREFIX)) { + // Show detailed connection error + String errorDetail = message.substring(ForgeConstants.CONN_ERROR_PREFIX.length()); + closeConn(errorDetail); + return; + } else if (ForgeConstants.INVALID_HOST_COMMAND.equals(message)) { closeConn(Forge.getLocalizer().getMessage("lblDetectedInvalidHostAddress", url)); return; } diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index ebbbf217683..6c5daf498cf 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -311,6 +311,12 @@ AresetMatchScreenLayout=This will reset the layout of the Match screen.\n If you TresetMatchScreenLayout=Reset Match Screen Layout OKresetMatchScreenLayout=Match Screen layout has been reset. UnableConnectToServer=Unable to connect to server with host {0}. +lblConnectionError=Connection Error +lblConnectionFailedTo=Failed to connect to {0}:{1} +lblConnectionRefused=Connection refused. Please verify:\n- The host is running and accepting connections\n- The port number is correct\n- No firewall is blocking the connection +lblUnknownHost=Unknown host. Please check the hostname or IP address is correct. +lblConnectionTimeout=Connection timed out. The server may be unreachable or behind a firewall. +lblNoRouteToHost=No route to host. Please check your network connection. #EMenuGroup.java lblSanctionedFormats=Sanctioned Formats lblOnlineMultiplayer=Online Multiplayer @@ -1515,10 +1521,13 @@ lblConcedeCurrentGame=This will concede the current game and you will lose.\n\nC lblConcedeTitle=Concede Game? lblConcede=Concede lblWaitingforActions=Waiting for actions... +lblWaitingForPlayer=Waiting for {0}... +lblWaitingForPlayerWithTime=Waiting for {0}... ({1}) lblCloseGameSpectator=This will close this game and you will not be able to resume watching it.\n\nClose anyway? lblCloseGame=Close Game? lblWaitingForOpponent=Waiting for opponent... lblYieldingUntilEndOfTurn=Yielding until end of turn.\nYou may cancel this yield to take an action. +lblYieldingWaitingForPlayer=Yielding until end of turn. Waiting for {0}...\nCancel to take an action. lblStopWatching=Stop Watching lblEnterNumberBetweenMinAndMax=Enter a number between {0} and {1}: lblEnterNumberGreaterThanOrEqualsToMin=Enter a number greater than or equal to {0}: diff --git a/forge-gui/src/main/java/forge/gamemodes/match/input/InputLockUI.java b/forge-gui/src/main/java/forge/gamemodes/match/input/InputLockUI.java index be1a4600bcb..296aae8b4b6 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/input/InputLockUI.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/input/InputLockUI.java @@ -1,10 +1,15 @@ package forge.gamemodes.match.input; +import forge.game.Game; +import forge.game.GameView; import forge.game.card.Card; +import forge.game.phase.PhaseHandler; import forge.game.player.Player; import forge.game.player.PlayerView; import forge.game.spellability.SpellAbility; import forge.gui.FThreads; +import forge.gui.GuiBase; +import forge.gui.interfaces.IGuiGame; import forge.player.PlayerControllerHuman; import forge.util.ITriggerEvent; import forge.util.Localizer; @@ -15,6 +20,7 @@ public class InputLockUI implements Input { private final AtomicInteger iCall = new AtomicInteger(); + private volatile long waitStartTime = 0; private final InputQueue inputQueue; private final PlayerControllerHuman controller; @@ -31,6 +37,7 @@ public PlayerView getOwner() { @Override public void showMessageInitial() { final int ixCall = 1 + iCall.getAndIncrement(); + waitStartTime = System.currentTimeMillis(); ThreadUtil.delay(500, new InputUpdater(ixCall)); } @@ -48,10 +55,14 @@ public InputUpdater(final int idxCall) { @Override public void run() { - if ( ixCall != iCall.get() || !isActive()) { + if (ixCall != iCall.get() || !isActive()) { return; } FThreads.invokeInEdtLater(showMessageFromEdt); + // Reschedule to update timer display every second (only in network games) + if (GuiBase.isNetworkplay()) { + ThreadUtil.delay(1000, this); + } } } @@ -59,10 +70,110 @@ public void run() { @Override public void run() { controller.getGui().updateButtons(InputLockUI.this.getOwner(), "", "", false, false, false); - showMessage(Localizer.getInstance().getMessage("lblWaitingforActions")); + showMessage(getWaitingMessage()); } }; + /** + * Get a descriptive waiting message. + * In network games, shows which player we're waiting for with elapsed time. + * In local games, shows the generic "Waiting for Actions" message. + */ + private String getWaitingMessage() { + Localizer localizer = Localizer.getInstance(); + + // In network games, show who we're waiting for + if (GuiBase.isNetworkplay()) { + String playerName = null; + + // First try: Get priority player from the local Game object (works on host) + Player player = controller.getPlayer(); + if (player != null) { + Game game = player.getGame(); + if (game != null && !game.isGameOver()) { + PhaseHandler ph = game.getPhaseHandler(); + if (ph != null) { + Player priorityPlayer = ph.getPriorityPlayer(); + if (priorityPlayer != null && priorityPlayer != player) { + playerName = priorityPlayer.getName(); + } + } + } + } + + // Fallback: Get priority player from the GameView (works on client) + // On the network client, the Game object is on the server, but GameView is synced + if (playerName == null) { + IGuiGame gui = controller.getGui(); + if (gui != null) { + GameView gameView = gui.getGameView(); + if (gameView != null && !gameView.isGameOver()) { + PlayerView priorityPlayer = findPriorityPlayer(gameView); + // Show the waiting message if priority player exists and is different from our player + PlayerView localPlayer = controller.getLocalPlayerView(); + if (priorityPlayer != null && (localPlayer == null || priorityPlayer.getId() != localPlayer.getId())) { + playerName = priorityPlayer.getName(); + } + } + } + } + + // Build the waiting message with player name and elapsed time + if (playerName != null) { + String timeStr = getElapsedTimeString(); + if (timeStr != null) { + return localizer.getMessage("lblWaitingForPlayerWithTime", playerName, timeStr); + } + return localizer.getMessage("lblWaitingForPlayer", playerName); + } + } + + // Default message for local games or when player info not available + return localizer.getMessage("lblWaitingforActions"); + } + + /** + * Get elapsed time as a formatted string (e.g., "5s", "1:23"). + * Returns null if wait just started (less than 2 seconds). + */ + private String getElapsedTimeString() { + if (waitStartTime == 0) { + return null; + } + long elapsedMs = System.currentTimeMillis() - waitStartTime; + long elapsedSec = elapsedMs / 1000; + + // Don't show timer for very short waits + if (elapsedSec < 2) { + return null; + } + + if (elapsedSec < 60) { + return elapsedSec + "s"; + } else { + long minutes = elapsedSec / 60; + long seconds = elapsedSec % 60; + return String.format("%d:%02d", minutes, seconds); + } + } + + /** + * Find the player with priority from the GameView. + * Checks PlayerView.getHasPriority() for each player. + * Falls back to getPlayerTurn() during game setup when no priority is set. + */ + private PlayerView findPriorityPlayer(GameView gameView) { + if (gameView.getPlayers() != null) { + for (PlayerView pv : gameView.getPlayers()) { + if (pv.getHasPriority()) { + return pv; + } + } + } + // Fallback to player turn during game setup (mulligan phase) + return gameView.getPlayerTurn(); + } + protected final boolean isActive() { return inputQueue.getInput() == this; } diff --git a/forge-gui/src/main/java/forge/gamemodes/net/NetConnectUtil.java b/forge-gui/src/main/java/forge/gamemodes/net/NetConnectUtil.java index 02bda81daa1..06f3569fff5 100644 --- a/forge-gui/src/main/java/forge/gamemodes/net/NetConnectUtil.java +++ b/forge-gui/src/main/java/forge/gamemodes/net/NetConnectUtil.java @@ -172,10 +172,47 @@ public ClientGameLobby getLobby() { client.connect(); } catch (Exception ex) { - //return a message to close the connection so we will not crash... - return new ChatMessage(null, ForgeConstants.CLOSE_CONN_COMMAND); + // Return error with details for GUI display + String errorDetail = getConnectionErrorMessage(ex, hostname, port); + return new ChatMessage(null, ForgeConstants.CONN_ERROR_PREFIX + errorDetail); } return new ChatMessage(null, Localizer.getInstance().getMessage("lblConnectedIPPort", hostname, String.valueOf(port))); } + + /** + * Generate a user-friendly error message for connection failures. + */ + private static String getConnectionErrorMessage(Exception ex, String hostname, int port) { + Localizer localizer = Localizer.getInstance(); + StringBuilder sb = new StringBuilder(); + + // Get the root cause for better error messages + Throwable cause = ex.getCause() != null ? ex.getCause() : ex; + String causeName = cause.getClass().getSimpleName(); + + sb.append(localizer.getMessage("lblConnectionFailedTo", hostname, String.valueOf(port))); + sb.append("\n\n"); + + // Provide specific messages for common error types + if (causeName.contains("ConnectException") || causeName.contains("ConnectionRefused")) { + sb.append(localizer.getMessage("lblConnectionRefused")); + } else if (causeName.contains("UnknownHost")) { + sb.append(localizer.getMessage("lblUnknownHost")); + } else if (causeName.contains("Timeout") || causeName.contains("TimedOut")) { + sb.append(localizer.getMessage("lblConnectionTimeout")); + } else if (causeName.contains("NoRouteToHost")) { + sb.append(localizer.getMessage("lblNoRouteToHost")); + } else { + // Generic error with the exception message + String msg = cause.getMessage(); + if (msg != null && !msg.isEmpty()) { + sb.append(msg); + } else { + sb.append(causeName); + } + } + + return sb.toString(); + } } diff --git a/forge-gui/src/main/java/forge/localinstance/properties/ForgeConstants.java b/forge-gui/src/main/java/forge/localinstance/properties/ForgeConstants.java index 5febf260079..920f85dc5a5 100644 --- a/forge-gui/src/main/java/forge/localinstance/properties/ForgeConstants.java +++ b/forge-gui/src/main/java/forge/localinstance/properties/ForgeConstants.java @@ -272,6 +272,7 @@ public final class ForgeConstants { public static final String ITEM_VIEW_PREFS_FILE = USER_PREFS_DIR + "item_view.preferences"; public static final String CLOSE_CONN_COMMAND = "<<_EM_ESOLC_<<"; public static final String INVALID_HOST_COMMAND = "<<_TSOH_DILAVNI_<<"; + public static final String CONN_ERROR_PREFIX = "<<_CONN_ERROR_>>:"; // data that has defaults in the program dir but overrides/additions in the user dir private static final String _DEFAULTS_DIR = RES_DIR + "defaults" + PATH_SEPARATOR; From fd695a720bee6fa4797a0d13d7e6c4e806a829b6 Mon Sep 17 00:00:00 2001 From: MostCromulent Date: Thu, 5 Feb 2026 06:55:47 +1030 Subject: [PATCH 2/7] Fix view separation and i18n issues in network UI - Use GameView data only in findWaitingForPlayerName() to work on both host and client (fixes null Game on network clients) - Add findPriorityPlayer() using PlayerView.getHasPriority() - Use localized string prefixes in isWaitingMessage() instead of hardcoded English strings - Add volatile to timer fields for thread safety Co-Authored-By: Claude Opus 4.5 --- .../java/forge/screens/match/CMatchUI.java | 88 +++++++++++-------- 1 file changed, 52 insertions(+), 36 deletions(-) diff --git a/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java b/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java index a6b7c51f698..bdda4d1fd5c 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java +++ b/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java @@ -177,9 +177,10 @@ public final class CMatchUI private int nextNotifiableStackIndex = 0; // Timer for "Waiting for..." messages in network games + // Fields accessed from Swing Timer callbacks must be volatile for thread safety private javax.swing.Timer waitingTimer; - private long waitingStartTime; - private String waitingBaseMessage; + private volatile long waitingStartTime; + private volatile String waitingBaseMessage; public CMatchUI() { this.view = new VMatchUI(this); @@ -1035,44 +1036,31 @@ private String enhanceWaitingMessageForHost(final String message, final PlayerVi /** * Find the name of the player we're waiting for. + * Uses GameView data only to work correctly on both host and client. * Checks priority player, turn player, or falls back to finding other players. */ private String findWaitingForPlayerName(GameView gameView, PlayerView forPlayer) { - // Try to get priority player from Game object (host has access to this) - forge.game.Game game = gameView.getGame(); - if (game != null && !game.isGameOver()) { - forge.game.phase.PhaseHandler ph = game.getPhaseHandler(); - if (ph != null) { - forge.game.player.Player priorityPlayer = ph.getPriorityPlayer(); - if (priorityPlayer == null) { - priorityPlayer = ph.getPlayerTurn(); - } - // During mulligan, both may be null - find other players - if (priorityPlayer == null) { - // If forPlayer is known, find someone else - if (forPlayer != null) { - for (forge.game.player.Player p : game.getPlayers()) { - if (p.getView().getId() != forPlayer.getId()) { - return p.getName(); - } - } - } - // If forPlayer is null, find any non-local player - for (forge.game.player.Player p : game.getPlayers()) { - if (!isLocalPlayer(p.getView())) { - return p.getName(); - } - } - } - if (priorityPlayer != null && (forPlayer == null || priorityPlayer.getView().getId() != forPlayer.getId())) { - return priorityPlayer.getName(); - } - } + if (gameView.isGameOver()) { + return null; } - // Fallback: use GameView's player list to find non-local players + // First, try to find the player with priority from GameView + PlayerView priorityPlayer = findPriorityPlayer(gameView); + + // If we found a priority player different from forPlayer, use them + if (priorityPlayer != null && (forPlayer == null || priorityPlayer.getId() != forPlayer.getId())) { + return priorityPlayer.getName(); + } + + // During mulligan or game setup, priority may not be set + // Fall back to finding any non-local player (in network games, that's who we're waiting for) if (gameView.getPlayers() != null) { for (PlayerView pv : gameView.getPlayers()) { + // Skip forPlayer if specified + if (forPlayer != null && pv.getId() == forPlayer.getId()) { + continue; + } + // Skip local players - we want the remote player we're waiting for if (!isLocalPlayer(pv)) { return pv.getName(); } @@ -1082,15 +1070,43 @@ private String findWaitingForPlayerName(GameView gameView, PlayerView forPlayer) return null; } + /** + * Find the player with priority from the GameView. + * Checks PlayerView.getHasPriority() for each player. + * Falls back to getPlayerTurn() during game setup when no priority is set. + */ + private PlayerView findPriorityPlayer(GameView gameView) { + if (gameView.getPlayers() != null) { + for (PlayerView pv : gameView.getPlayers()) { + if (pv.getHasPriority()) { + return pv; + } + } + } + // Fallback to player turn during game setup (mulligan phase) + return gameView.getPlayerTurn(); + } + private boolean isWaitingMessage(String message) { Localizer localizer = Localizer.getInstance(); String waitingForOpponent = localizer.getMessage("lblWaitingForOpponent"); String waitingForActions = localizer.getMessage("lblWaitingforActions"); - // Also match "Waiting for PlayerName..." and "Yielding ... Waiting for PlayerName..." patterns + + // Extract the localized prefix from "Waiting for {0}..." to match player-specific messages + // This avoids hardcoding English strings and works with any locale + String waitingForPlayerTemplate = localizer.getMessage("lblWaitingForPlayer", ""); + // The template with empty string gives us the prefix before the player name + // e.g., "Waiting for ..." -> we use "Waiting for " as prefix + String localizedWaitingPrefix = waitingForPlayerTemplate.replace("...", "").trim(); + + // Also get the yielding+waiting pattern for completeness + String yieldingTemplate = localizer.getMessage("lblYieldingWaitingForPlayer", ""); + return message.equals(waitingForOpponent) || message.equals(waitingForActions) - || message.startsWith("Waiting for ") - || message.contains("Waiting for "); + || message.startsWith(localizedWaitingPrefix) + || message.contains(localizedWaitingPrefix) + || message.startsWith(yieldingTemplate.split("\\{")[0]); } private String extractBaseWaitingMessage(String message) { From 9cfa3586354be25f31b77bc7c0c91b2499b237f0 Mon Sep 17 00:00:00 2001 From: MostCromulent Date: Fri, 6 Feb 2026 06:39:08 +1030 Subject: [PATCH 3/7] Refactor waiting timer to AbstractGuiGame (PR 9671 feedback) Addresses review feedback from tool4ever: consolidate timer logic into the existing awaitNextInput() mechanism in AbstractGuiGame instead of adding separate timer systems in InputLockUI and CMatchUI. Changes: - Revert InputLockUI.java to master (remove ~100 lines of timer code) - Revert CMatchUI.java to master (remove ~185 lines of timer/prompt code) - Enhance AbstractGuiGame.updatePromptForAwait() with: - Player name lookup (shows "Waiting for [Player]..." in network games) - Elapsed time display (shows "(5s)" or "(1:23)" suffix) - 1-second reschedule loop for network games only - Remove unused lblYieldingWaitingForPlayer from en-US.properties Benefits: - 1 timer system instead of 3 (reuses existing awaitNextInputTimer) - Platform-neutral: works on mobile automatically - No duplicate code: single findWaitingForPlayerName() implementation - Smaller diff: 155 lines total vs 369 before refactor Co-Authored-By: Claude Opus 4.5 --- .../java/forge/screens/match/CMatchUI.java | 192 +----------------- forge-gui/res/languages/en-US.properties | 1 - .../gamemodes/match/AbstractGuiGame.java | 93 ++++++++- .../gamemodes/match/input/InputLockUI.java | 115 +---------- 4 files changed, 94 insertions(+), 307 deletions(-) diff --git a/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java b/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java index bdda4d1fd5c..eb0c753b8be 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java +++ b/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java @@ -176,12 +176,6 @@ public final class CMatchUI private final CStack cStack = new CStack(this); private int nextNotifiableStackIndex = 0; - // Timer for "Waiting for..." messages in network games - // Fields accessed from Swing Timer callbacks must be volatile for thread safety - private javax.swing.Timer waitingTimer; - private volatile long waitingStartTime; - private volatile String waitingBaseMessage; - public CMatchUI() { this.view = new VMatchUI(this); this.screen = FScreen.getMatchScreen(this, view); @@ -825,7 +819,6 @@ public void enableOverlay() { @Override public void finishGame() { - stopWaitingTimer(); // Clean up waiting timer FloatingZone.closeAll(); //ensure floating card areas cleared and closed after the game final GameView gameView = getGameView(); if (hasLocalPlayers() || gameView.isMatchOver()) { @@ -970,190 +963,7 @@ public void popupMenuCanceled(PopupMenuEvent e) { @Override public void showPromptMessage(final PlayerView playerView, final String message) { - // First, enhance generic waiting messages with player name (for host) - String enhancedMessage = enhanceWaitingMessageForHost(message, playerView); - - // Handle timer for "Waiting for..." messages in network games - if (GuiBase.isNetworkplay() && enhancedMessage != null && isWaitingMessage(enhancedMessage)) { - // Check if this is the same base message (just timer update) or a new waiting message - String baseMsg = extractBaseWaitingMessage(enhancedMessage); - if (!baseMsg.equals(waitingBaseMessage)) { - // New waiting message - start timer - stopWaitingTimer(); - waitingBaseMessage = baseMsg; - waitingStartTime = System.currentTimeMillis(); - startWaitingTimer(); - } - // Display message with current timer - cPrompt.setMessage(getWaitingMessageWithTimer()); - } else { - // Not a waiting message - stop timer and show message directly - stopWaitingTimer(); - waitingBaseMessage = null; - cPrompt.setMessage(enhancedMessage); - } - } - - /** - * Enhance generic "Waiting for opponent" and "Yielding" messages with the actual player name. - * This is used on the host side where messages aren't sent through NetGuiGame. - */ - private String enhanceWaitingMessageForHost(final String message, final PlayerView forPlayer) { - if (!GuiBase.isNetworkplay() || message == null || message.isEmpty()) { - return message; - } - - Localizer localizer = Localizer.getInstance(); - String waitingForOpponent = localizer.getMessage("lblWaitingForOpponent"); - String yieldingMessage = localizer.getMessage("lblYieldingUntilEndOfTurn"); - - boolean isWaitingOpponent = message.equals(waitingForOpponent); - boolean isYielding = message.equals(yieldingMessage); - - if (!isWaitingOpponent && !isYielding) { - return message; - } - - // Get the priority player from the Game object - GameView gameView = getGameView(); - if (gameView == null) { - return message; - } - - // Find the player we're waiting for - String waitingForName = findWaitingForPlayerName(gameView, forPlayer); - if (waitingForName == null) { - return message; - } - - // Return enhanced message with player name - if (isYielding) { - return localizer.getMessage("lblYieldingWaitingForPlayer", waitingForName); - } else { - return localizer.getMessage("lblWaitingForPlayer", waitingForName); - } - } - - /** - * Find the name of the player we're waiting for. - * Uses GameView data only to work correctly on both host and client. - * Checks priority player, turn player, or falls back to finding other players. - */ - private String findWaitingForPlayerName(GameView gameView, PlayerView forPlayer) { - if (gameView.isGameOver()) { - return null; - } - - // First, try to find the player with priority from GameView - PlayerView priorityPlayer = findPriorityPlayer(gameView); - - // If we found a priority player different from forPlayer, use them - if (priorityPlayer != null && (forPlayer == null || priorityPlayer.getId() != forPlayer.getId())) { - return priorityPlayer.getName(); - } - - // During mulligan or game setup, priority may not be set - // Fall back to finding any non-local player (in network games, that's who we're waiting for) - if (gameView.getPlayers() != null) { - for (PlayerView pv : gameView.getPlayers()) { - // Skip forPlayer if specified - if (forPlayer != null && pv.getId() == forPlayer.getId()) { - continue; - } - // Skip local players - we want the remote player we're waiting for - if (!isLocalPlayer(pv)) { - return pv.getName(); - } - } - } - - return null; - } - - /** - * Find the player with priority from the GameView. - * Checks PlayerView.getHasPriority() for each player. - * Falls back to getPlayerTurn() during game setup when no priority is set. - */ - private PlayerView findPriorityPlayer(GameView gameView) { - if (gameView.getPlayers() != null) { - for (PlayerView pv : gameView.getPlayers()) { - if (pv.getHasPriority()) { - return pv; - } - } - } - // Fallback to player turn during game setup (mulligan phase) - return gameView.getPlayerTurn(); - } - - private boolean isWaitingMessage(String message) { - Localizer localizer = Localizer.getInstance(); - String waitingForOpponent = localizer.getMessage("lblWaitingForOpponent"); - String waitingForActions = localizer.getMessage("lblWaitingforActions"); - - // Extract the localized prefix from "Waiting for {0}..." to match player-specific messages - // This avoids hardcoding English strings and works with any locale - String waitingForPlayerTemplate = localizer.getMessage("lblWaitingForPlayer", ""); - // The template with empty string gives us the prefix before the player name - // e.g., "Waiting for ..." -> we use "Waiting for " as prefix - String localizedWaitingPrefix = waitingForPlayerTemplate.replace("...", "").trim(); - - // Also get the yielding+waiting pattern for completeness - String yieldingTemplate = localizer.getMessage("lblYieldingWaitingForPlayer", ""); - - return message.equals(waitingForOpponent) - || message.equals(waitingForActions) - || message.startsWith(localizedWaitingPrefix) - || message.contains(localizedWaitingPrefix) - || message.startsWith(yieldingTemplate.split("\\{")[0]); - } - - private String extractBaseWaitingMessage(String message) { - // Remove any existing timer suffix like " (5s)" or " (1:23)" - return message.replaceAll(" \\(\\d+s\\)$", "").replaceAll(" \\(\\d+:\\d{2}\\)$", ""); - } - - private String getWaitingMessageWithTimer() { - if (waitingBaseMessage == null) { - return ""; - } - long elapsedMs = System.currentTimeMillis() - waitingStartTime; - long elapsedSec = elapsedMs / 1000; - - // Don't show timer for very short waits - if (elapsedSec < 2) { - return waitingBaseMessage; - } - - String timeStr; - if (elapsedSec < 60) { - timeStr = elapsedSec + "s"; - } else { - long minutes = elapsedSec / 60; - long seconds = elapsedSec % 60; - timeStr = String.format("%d:%02d", minutes, seconds); - } - return waitingBaseMessage + " (" + timeStr + ")"; - } - - private void startWaitingTimer() { - if (waitingTimer != null) { - return; // Already running - } - waitingTimer = new javax.swing.Timer(1000, e -> { - if (waitingBaseMessage != null) { - cPrompt.setMessage(getWaitingMessageWithTimer()); - } - }); - waitingTimer.start(); - } - - private void stopWaitingTimer() { - if (waitingTimer != null) { - waitingTimer.stop(); - waitingTimer = null; - } + cPrompt.setMessage(message); } @Override diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index c380b7bd403..c78a42aab4e 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -1532,7 +1532,6 @@ lblCloseGameSpectator=This will close this game and you will not be able to resu lblCloseGame=Close Game? lblWaitingForOpponent=Waiting for opponent... lblYieldingUntilEndOfTurn=Yielding until end of turn.\nYou may cancel this yield to take an action. -lblYieldingWaitingForPlayer=Yielding until end of turn. Waiting for {0}...\nCancel to take an action. lblStopWatching=Stop Watching lblEnterNumberBetweenMinAndMax=Enter a number between {0} and {1}: lblEnterNumberGreaterThanOrEqualsToMin=Enter a number greater than or equal to {0}: diff --git a/forge-gui/src/main/java/forge/gamemodes/match/AbstractGuiGame.java b/forge-gui/src/main/java/forge/gamemodes/match/AbstractGuiGame.java index 333981e1311..68b2fc91ca7 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/AbstractGuiGame.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/AbstractGuiGame.java @@ -446,10 +446,12 @@ public final boolean mayAutoPass(final PlayerView player) { private Timer awaitNextInputTimer; private TimerTask awaitNextInputTask; + private volatile long awaitStartTime; @Override public final void awaitNextInput() { checkAwaitNextInputTimer(); + awaitStartTime = System.currentTimeMillis(); //delay updating prompt to await next input briefly so buttons don't flicker disabled then enabled awaitNextInputTask = new TimerTask() { @Override @@ -459,7 +461,12 @@ public void run() { synchronized (awaitNextInputTimer) { if (awaitNextInputTask != null) { updatePromptForAwait(getCurrentPlayer()); - awaitNextInputTask = null; + // In network games, reschedule every second to update elapsed time + if (GuiBase.isNetworkplay()) { + scheduleTimerUpdate(); + } else { + awaitNextInputTask = null; + } } } }); @@ -467,6 +474,29 @@ public void run() { }; awaitNextInputTimer.schedule(awaitNextInputTask, 250); } + + private void scheduleTimerUpdate() { + awaitNextInputTask = new TimerTask() { + @Override + public void run() { + FThreads.invokeInEdtLater(() -> { + checkAwaitNextInputTimer(); + synchronized (awaitNextInputTimer) { + if (awaitNextInputTask != null) { + showPromptMessage(getCurrentPlayer(), getWaitingMessage(getCurrentPlayer())); + scheduleTimerUpdate(); + } + } + }); + } + }; + try { + awaitNextInputTimer.schedule(awaitNextInputTask, 1000); + } catch (final IllegalStateException ex) { + // Timer was cancelled between check and schedule + } + } + private void checkAwaitNextInputTimer() { if (awaitNextInputTimer == null) { String name = "?"; @@ -477,10 +507,68 @@ private void checkAwaitNextInputTimer() { } protected final void updatePromptForAwait(final PlayerView playerView) { - showPromptMessage(playerView, Localizer.getInstance().getMessage("lblWaitingForOpponent")); + showPromptMessage(playerView, getWaitingMessage(playerView)); updateButtons(playerView, false, false, false); } + private String getWaitingMessage(final PlayerView forPlayer) { + Localizer localizer = Localizer.getInstance(); + + if (GuiBase.isNetworkplay() && gameView != null && !gameView.isGameOver()) { + String name = findWaitingForPlayerName(forPlayer); + if (name != null) { + String timeStr = getElapsedTimeString(); + if (timeStr != null) { + return localizer.getMessage("lblWaitingForPlayerWithTime", name, timeStr); + } + return localizer.getMessage("lblWaitingForPlayer", name); + } + } + + return localizer.getMessage("lblWaitingForOpponent"); + } + + private String findWaitingForPlayerName(final PlayerView forPlayer) { + if (gameView.getPlayers() != null) { + for (PlayerView pv : gameView.getPlayers()) { + if (pv.getHasPriority() && (forPlayer == null || pv.getId() != forPlayer.getId())) { + return pv.getName(); + } + } + } + // Fallback to turn player during mulligan/setup + PlayerView turnPlayer = gameView.getPlayerTurn(); + if (turnPlayer != null && (forPlayer == null || turnPlayer.getId() != forPlayer.getId())) { + return turnPlayer.getName(); + } + // Fallback to any non-local player + if (gameView.getPlayers() != null) { + for (PlayerView pv : gameView.getPlayers()) { + if (forPlayer != null && pv.getId() == forPlayer.getId()) { + continue; + } + if (!isLocalPlayer(pv)) { + return pv.getName(); + } + } + } + return null; + } + + private String getElapsedTimeString() { + if (awaitStartTime == 0) { + return null; + } + long elapsedSec = (System.currentTimeMillis() - awaitStartTime) / 1000; + if (elapsedSec < 2) { + return null; + } + if (elapsedSec < 60) { + return elapsedSec + "s"; + } + return String.format("%d:%02d", elapsedSec / 60, elapsedSec % 60); + } + @Override public final void cancelAwaitNextInput() { if (awaitNextInputTimer == null) { @@ -495,6 +583,7 @@ public final void cancelAwaitNextInput() { awaitNextInputTask = null; } } + awaitStartTime = 0; } @Override diff --git a/forge-gui/src/main/java/forge/gamemodes/match/input/InputLockUI.java b/forge-gui/src/main/java/forge/gamemodes/match/input/InputLockUI.java index 296aae8b4b6..be1a4600bcb 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/input/InputLockUI.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/input/InputLockUI.java @@ -1,15 +1,10 @@ package forge.gamemodes.match.input; -import forge.game.Game; -import forge.game.GameView; import forge.game.card.Card; -import forge.game.phase.PhaseHandler; import forge.game.player.Player; import forge.game.player.PlayerView; import forge.game.spellability.SpellAbility; import forge.gui.FThreads; -import forge.gui.GuiBase; -import forge.gui.interfaces.IGuiGame; import forge.player.PlayerControllerHuman; import forge.util.ITriggerEvent; import forge.util.Localizer; @@ -20,7 +15,6 @@ public class InputLockUI implements Input { private final AtomicInteger iCall = new AtomicInteger(); - private volatile long waitStartTime = 0; private final InputQueue inputQueue; private final PlayerControllerHuman controller; @@ -37,7 +31,6 @@ public PlayerView getOwner() { @Override public void showMessageInitial() { final int ixCall = 1 + iCall.getAndIncrement(); - waitStartTime = System.currentTimeMillis(); ThreadUtil.delay(500, new InputUpdater(ixCall)); } @@ -55,14 +48,10 @@ public InputUpdater(final int idxCall) { @Override public void run() { - if (ixCall != iCall.get() || !isActive()) { + if ( ixCall != iCall.get() || !isActive()) { return; } FThreads.invokeInEdtLater(showMessageFromEdt); - // Reschedule to update timer display every second (only in network games) - if (GuiBase.isNetworkplay()) { - ThreadUtil.delay(1000, this); - } } } @@ -70,110 +59,10 @@ public void run() { @Override public void run() { controller.getGui().updateButtons(InputLockUI.this.getOwner(), "", "", false, false, false); - showMessage(getWaitingMessage()); + showMessage(Localizer.getInstance().getMessage("lblWaitingforActions")); } }; - /** - * Get a descriptive waiting message. - * In network games, shows which player we're waiting for with elapsed time. - * In local games, shows the generic "Waiting for Actions" message. - */ - private String getWaitingMessage() { - Localizer localizer = Localizer.getInstance(); - - // In network games, show who we're waiting for - if (GuiBase.isNetworkplay()) { - String playerName = null; - - // First try: Get priority player from the local Game object (works on host) - Player player = controller.getPlayer(); - if (player != null) { - Game game = player.getGame(); - if (game != null && !game.isGameOver()) { - PhaseHandler ph = game.getPhaseHandler(); - if (ph != null) { - Player priorityPlayer = ph.getPriorityPlayer(); - if (priorityPlayer != null && priorityPlayer != player) { - playerName = priorityPlayer.getName(); - } - } - } - } - - // Fallback: Get priority player from the GameView (works on client) - // On the network client, the Game object is on the server, but GameView is synced - if (playerName == null) { - IGuiGame gui = controller.getGui(); - if (gui != null) { - GameView gameView = gui.getGameView(); - if (gameView != null && !gameView.isGameOver()) { - PlayerView priorityPlayer = findPriorityPlayer(gameView); - // Show the waiting message if priority player exists and is different from our player - PlayerView localPlayer = controller.getLocalPlayerView(); - if (priorityPlayer != null && (localPlayer == null || priorityPlayer.getId() != localPlayer.getId())) { - playerName = priorityPlayer.getName(); - } - } - } - } - - // Build the waiting message with player name and elapsed time - if (playerName != null) { - String timeStr = getElapsedTimeString(); - if (timeStr != null) { - return localizer.getMessage("lblWaitingForPlayerWithTime", playerName, timeStr); - } - return localizer.getMessage("lblWaitingForPlayer", playerName); - } - } - - // Default message for local games or when player info not available - return localizer.getMessage("lblWaitingforActions"); - } - - /** - * Get elapsed time as a formatted string (e.g., "5s", "1:23"). - * Returns null if wait just started (less than 2 seconds). - */ - private String getElapsedTimeString() { - if (waitStartTime == 0) { - return null; - } - long elapsedMs = System.currentTimeMillis() - waitStartTime; - long elapsedSec = elapsedMs / 1000; - - // Don't show timer for very short waits - if (elapsedSec < 2) { - return null; - } - - if (elapsedSec < 60) { - return elapsedSec + "s"; - } else { - long minutes = elapsedSec / 60; - long seconds = elapsedSec % 60; - return String.format("%d:%02d", minutes, seconds); - } - } - - /** - * Find the player with priority from the GameView. - * Checks PlayerView.getHasPriority() for each player. - * Falls back to getPlayerTurn() during game setup when no priority is set. - */ - private PlayerView findPriorityPlayer(GameView gameView) { - if (gameView.getPlayers() != null) { - for (PlayerView pv : gameView.getPlayers()) { - if (pv.getHasPriority()) { - return pv; - } - } - } - // Fallback to player turn during game setup (mulligan phase) - return gameView.getPlayerTurn(); - } - protected final boolean isActive() { return inputQueue.getInput() == this; } From 2afb0470ffebca85187f47b914ffa46fa871342d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Feb 2026 22:32:38 +0000 Subject: [PATCH 4/7] Revert "Refactor waiting timer to AbstractGuiGame (PR 9671 feedback)" This reverts commit 9cfa3586354be25f31b77bc7c0c91b2499b237f0. --- .../java/forge/screens/match/CMatchUI.java | 192 +++++++++++++++++- forge-gui/res/languages/en-US.properties | 1 + .../gamemodes/match/AbstractGuiGame.java | 93 +-------- .../gamemodes/match/input/InputLockUI.java | 115 ++++++++++- 4 files changed, 307 insertions(+), 94 deletions(-) diff --git a/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java b/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java index eb0c753b8be..bdda4d1fd5c 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java +++ b/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java @@ -176,6 +176,12 @@ public final class CMatchUI private final CStack cStack = new CStack(this); private int nextNotifiableStackIndex = 0; + // Timer for "Waiting for..." messages in network games + // Fields accessed from Swing Timer callbacks must be volatile for thread safety + private javax.swing.Timer waitingTimer; + private volatile long waitingStartTime; + private volatile String waitingBaseMessage; + public CMatchUI() { this.view = new VMatchUI(this); this.screen = FScreen.getMatchScreen(this, view); @@ -819,6 +825,7 @@ public void enableOverlay() { @Override public void finishGame() { + stopWaitingTimer(); // Clean up waiting timer FloatingZone.closeAll(); //ensure floating card areas cleared and closed after the game final GameView gameView = getGameView(); if (hasLocalPlayers() || gameView.isMatchOver()) { @@ -963,7 +970,190 @@ public void popupMenuCanceled(PopupMenuEvent e) { @Override public void showPromptMessage(final PlayerView playerView, final String message) { - cPrompt.setMessage(message); + // First, enhance generic waiting messages with player name (for host) + String enhancedMessage = enhanceWaitingMessageForHost(message, playerView); + + // Handle timer for "Waiting for..." messages in network games + if (GuiBase.isNetworkplay() && enhancedMessage != null && isWaitingMessage(enhancedMessage)) { + // Check if this is the same base message (just timer update) or a new waiting message + String baseMsg = extractBaseWaitingMessage(enhancedMessage); + if (!baseMsg.equals(waitingBaseMessage)) { + // New waiting message - start timer + stopWaitingTimer(); + waitingBaseMessage = baseMsg; + waitingStartTime = System.currentTimeMillis(); + startWaitingTimer(); + } + // Display message with current timer + cPrompt.setMessage(getWaitingMessageWithTimer()); + } else { + // Not a waiting message - stop timer and show message directly + stopWaitingTimer(); + waitingBaseMessage = null; + cPrompt.setMessage(enhancedMessage); + } + } + + /** + * Enhance generic "Waiting for opponent" and "Yielding" messages with the actual player name. + * This is used on the host side where messages aren't sent through NetGuiGame. + */ + private String enhanceWaitingMessageForHost(final String message, final PlayerView forPlayer) { + if (!GuiBase.isNetworkplay() || message == null || message.isEmpty()) { + return message; + } + + Localizer localizer = Localizer.getInstance(); + String waitingForOpponent = localizer.getMessage("lblWaitingForOpponent"); + String yieldingMessage = localizer.getMessage("lblYieldingUntilEndOfTurn"); + + boolean isWaitingOpponent = message.equals(waitingForOpponent); + boolean isYielding = message.equals(yieldingMessage); + + if (!isWaitingOpponent && !isYielding) { + return message; + } + + // Get the priority player from the Game object + GameView gameView = getGameView(); + if (gameView == null) { + return message; + } + + // Find the player we're waiting for + String waitingForName = findWaitingForPlayerName(gameView, forPlayer); + if (waitingForName == null) { + return message; + } + + // Return enhanced message with player name + if (isYielding) { + return localizer.getMessage("lblYieldingWaitingForPlayer", waitingForName); + } else { + return localizer.getMessage("lblWaitingForPlayer", waitingForName); + } + } + + /** + * Find the name of the player we're waiting for. + * Uses GameView data only to work correctly on both host and client. + * Checks priority player, turn player, or falls back to finding other players. + */ + private String findWaitingForPlayerName(GameView gameView, PlayerView forPlayer) { + if (gameView.isGameOver()) { + return null; + } + + // First, try to find the player with priority from GameView + PlayerView priorityPlayer = findPriorityPlayer(gameView); + + // If we found a priority player different from forPlayer, use them + if (priorityPlayer != null && (forPlayer == null || priorityPlayer.getId() != forPlayer.getId())) { + return priorityPlayer.getName(); + } + + // During mulligan or game setup, priority may not be set + // Fall back to finding any non-local player (in network games, that's who we're waiting for) + if (gameView.getPlayers() != null) { + for (PlayerView pv : gameView.getPlayers()) { + // Skip forPlayer if specified + if (forPlayer != null && pv.getId() == forPlayer.getId()) { + continue; + } + // Skip local players - we want the remote player we're waiting for + if (!isLocalPlayer(pv)) { + return pv.getName(); + } + } + } + + return null; + } + + /** + * Find the player with priority from the GameView. + * Checks PlayerView.getHasPriority() for each player. + * Falls back to getPlayerTurn() during game setup when no priority is set. + */ + private PlayerView findPriorityPlayer(GameView gameView) { + if (gameView.getPlayers() != null) { + for (PlayerView pv : gameView.getPlayers()) { + if (pv.getHasPriority()) { + return pv; + } + } + } + // Fallback to player turn during game setup (mulligan phase) + return gameView.getPlayerTurn(); + } + + private boolean isWaitingMessage(String message) { + Localizer localizer = Localizer.getInstance(); + String waitingForOpponent = localizer.getMessage("lblWaitingForOpponent"); + String waitingForActions = localizer.getMessage("lblWaitingforActions"); + + // Extract the localized prefix from "Waiting for {0}..." to match player-specific messages + // This avoids hardcoding English strings and works with any locale + String waitingForPlayerTemplate = localizer.getMessage("lblWaitingForPlayer", ""); + // The template with empty string gives us the prefix before the player name + // e.g., "Waiting for ..." -> we use "Waiting for " as prefix + String localizedWaitingPrefix = waitingForPlayerTemplate.replace("...", "").trim(); + + // Also get the yielding+waiting pattern for completeness + String yieldingTemplate = localizer.getMessage("lblYieldingWaitingForPlayer", ""); + + return message.equals(waitingForOpponent) + || message.equals(waitingForActions) + || message.startsWith(localizedWaitingPrefix) + || message.contains(localizedWaitingPrefix) + || message.startsWith(yieldingTemplate.split("\\{")[0]); + } + + private String extractBaseWaitingMessage(String message) { + // Remove any existing timer suffix like " (5s)" or " (1:23)" + return message.replaceAll(" \\(\\d+s\\)$", "").replaceAll(" \\(\\d+:\\d{2}\\)$", ""); + } + + private String getWaitingMessageWithTimer() { + if (waitingBaseMessage == null) { + return ""; + } + long elapsedMs = System.currentTimeMillis() - waitingStartTime; + long elapsedSec = elapsedMs / 1000; + + // Don't show timer for very short waits + if (elapsedSec < 2) { + return waitingBaseMessage; + } + + String timeStr; + if (elapsedSec < 60) { + timeStr = elapsedSec + "s"; + } else { + long minutes = elapsedSec / 60; + long seconds = elapsedSec % 60; + timeStr = String.format("%d:%02d", minutes, seconds); + } + return waitingBaseMessage + " (" + timeStr + ")"; + } + + private void startWaitingTimer() { + if (waitingTimer != null) { + return; // Already running + } + waitingTimer = new javax.swing.Timer(1000, e -> { + if (waitingBaseMessage != null) { + cPrompt.setMessage(getWaitingMessageWithTimer()); + } + }); + waitingTimer.start(); + } + + private void stopWaitingTimer() { + if (waitingTimer != null) { + waitingTimer.stop(); + waitingTimer = null; + } } @Override diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index c78a42aab4e..c380b7bd403 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -1532,6 +1532,7 @@ lblCloseGameSpectator=This will close this game and you will not be able to resu lblCloseGame=Close Game? lblWaitingForOpponent=Waiting for opponent... lblYieldingUntilEndOfTurn=Yielding until end of turn.\nYou may cancel this yield to take an action. +lblYieldingWaitingForPlayer=Yielding until end of turn. Waiting for {0}...\nCancel to take an action. lblStopWatching=Stop Watching lblEnterNumberBetweenMinAndMax=Enter a number between {0} and {1}: lblEnterNumberGreaterThanOrEqualsToMin=Enter a number greater than or equal to {0}: diff --git a/forge-gui/src/main/java/forge/gamemodes/match/AbstractGuiGame.java b/forge-gui/src/main/java/forge/gamemodes/match/AbstractGuiGame.java index 68b2fc91ca7..333981e1311 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/AbstractGuiGame.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/AbstractGuiGame.java @@ -446,12 +446,10 @@ public final boolean mayAutoPass(final PlayerView player) { private Timer awaitNextInputTimer; private TimerTask awaitNextInputTask; - private volatile long awaitStartTime; @Override public final void awaitNextInput() { checkAwaitNextInputTimer(); - awaitStartTime = System.currentTimeMillis(); //delay updating prompt to await next input briefly so buttons don't flicker disabled then enabled awaitNextInputTask = new TimerTask() { @Override @@ -461,12 +459,7 @@ public void run() { synchronized (awaitNextInputTimer) { if (awaitNextInputTask != null) { updatePromptForAwait(getCurrentPlayer()); - // In network games, reschedule every second to update elapsed time - if (GuiBase.isNetworkplay()) { - scheduleTimerUpdate(); - } else { - awaitNextInputTask = null; - } + awaitNextInputTask = null; } } }); @@ -474,29 +467,6 @@ public void run() { }; awaitNextInputTimer.schedule(awaitNextInputTask, 250); } - - private void scheduleTimerUpdate() { - awaitNextInputTask = new TimerTask() { - @Override - public void run() { - FThreads.invokeInEdtLater(() -> { - checkAwaitNextInputTimer(); - synchronized (awaitNextInputTimer) { - if (awaitNextInputTask != null) { - showPromptMessage(getCurrentPlayer(), getWaitingMessage(getCurrentPlayer())); - scheduleTimerUpdate(); - } - } - }); - } - }; - try { - awaitNextInputTimer.schedule(awaitNextInputTask, 1000); - } catch (final IllegalStateException ex) { - // Timer was cancelled between check and schedule - } - } - private void checkAwaitNextInputTimer() { if (awaitNextInputTimer == null) { String name = "?"; @@ -507,68 +477,10 @@ private void checkAwaitNextInputTimer() { } protected final void updatePromptForAwait(final PlayerView playerView) { - showPromptMessage(playerView, getWaitingMessage(playerView)); + showPromptMessage(playerView, Localizer.getInstance().getMessage("lblWaitingForOpponent")); updateButtons(playerView, false, false, false); } - private String getWaitingMessage(final PlayerView forPlayer) { - Localizer localizer = Localizer.getInstance(); - - if (GuiBase.isNetworkplay() && gameView != null && !gameView.isGameOver()) { - String name = findWaitingForPlayerName(forPlayer); - if (name != null) { - String timeStr = getElapsedTimeString(); - if (timeStr != null) { - return localizer.getMessage("lblWaitingForPlayerWithTime", name, timeStr); - } - return localizer.getMessage("lblWaitingForPlayer", name); - } - } - - return localizer.getMessage("lblWaitingForOpponent"); - } - - private String findWaitingForPlayerName(final PlayerView forPlayer) { - if (gameView.getPlayers() != null) { - for (PlayerView pv : gameView.getPlayers()) { - if (pv.getHasPriority() && (forPlayer == null || pv.getId() != forPlayer.getId())) { - return pv.getName(); - } - } - } - // Fallback to turn player during mulligan/setup - PlayerView turnPlayer = gameView.getPlayerTurn(); - if (turnPlayer != null && (forPlayer == null || turnPlayer.getId() != forPlayer.getId())) { - return turnPlayer.getName(); - } - // Fallback to any non-local player - if (gameView.getPlayers() != null) { - for (PlayerView pv : gameView.getPlayers()) { - if (forPlayer != null && pv.getId() == forPlayer.getId()) { - continue; - } - if (!isLocalPlayer(pv)) { - return pv.getName(); - } - } - } - return null; - } - - private String getElapsedTimeString() { - if (awaitStartTime == 0) { - return null; - } - long elapsedSec = (System.currentTimeMillis() - awaitStartTime) / 1000; - if (elapsedSec < 2) { - return null; - } - if (elapsedSec < 60) { - return elapsedSec + "s"; - } - return String.format("%d:%02d", elapsedSec / 60, elapsedSec % 60); - } - @Override public final void cancelAwaitNextInput() { if (awaitNextInputTimer == null) { @@ -583,7 +495,6 @@ public final void cancelAwaitNextInput() { awaitNextInputTask = null; } } - awaitStartTime = 0; } @Override diff --git a/forge-gui/src/main/java/forge/gamemodes/match/input/InputLockUI.java b/forge-gui/src/main/java/forge/gamemodes/match/input/InputLockUI.java index be1a4600bcb..296aae8b4b6 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/input/InputLockUI.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/input/InputLockUI.java @@ -1,10 +1,15 @@ package forge.gamemodes.match.input; +import forge.game.Game; +import forge.game.GameView; import forge.game.card.Card; +import forge.game.phase.PhaseHandler; import forge.game.player.Player; import forge.game.player.PlayerView; import forge.game.spellability.SpellAbility; import forge.gui.FThreads; +import forge.gui.GuiBase; +import forge.gui.interfaces.IGuiGame; import forge.player.PlayerControllerHuman; import forge.util.ITriggerEvent; import forge.util.Localizer; @@ -15,6 +20,7 @@ public class InputLockUI implements Input { private final AtomicInteger iCall = new AtomicInteger(); + private volatile long waitStartTime = 0; private final InputQueue inputQueue; private final PlayerControllerHuman controller; @@ -31,6 +37,7 @@ public PlayerView getOwner() { @Override public void showMessageInitial() { final int ixCall = 1 + iCall.getAndIncrement(); + waitStartTime = System.currentTimeMillis(); ThreadUtil.delay(500, new InputUpdater(ixCall)); } @@ -48,10 +55,14 @@ public InputUpdater(final int idxCall) { @Override public void run() { - if ( ixCall != iCall.get() || !isActive()) { + if (ixCall != iCall.get() || !isActive()) { return; } FThreads.invokeInEdtLater(showMessageFromEdt); + // Reschedule to update timer display every second (only in network games) + if (GuiBase.isNetworkplay()) { + ThreadUtil.delay(1000, this); + } } } @@ -59,10 +70,110 @@ public void run() { @Override public void run() { controller.getGui().updateButtons(InputLockUI.this.getOwner(), "", "", false, false, false); - showMessage(Localizer.getInstance().getMessage("lblWaitingforActions")); + showMessage(getWaitingMessage()); } }; + /** + * Get a descriptive waiting message. + * In network games, shows which player we're waiting for with elapsed time. + * In local games, shows the generic "Waiting for Actions" message. + */ + private String getWaitingMessage() { + Localizer localizer = Localizer.getInstance(); + + // In network games, show who we're waiting for + if (GuiBase.isNetworkplay()) { + String playerName = null; + + // First try: Get priority player from the local Game object (works on host) + Player player = controller.getPlayer(); + if (player != null) { + Game game = player.getGame(); + if (game != null && !game.isGameOver()) { + PhaseHandler ph = game.getPhaseHandler(); + if (ph != null) { + Player priorityPlayer = ph.getPriorityPlayer(); + if (priorityPlayer != null && priorityPlayer != player) { + playerName = priorityPlayer.getName(); + } + } + } + } + + // Fallback: Get priority player from the GameView (works on client) + // On the network client, the Game object is on the server, but GameView is synced + if (playerName == null) { + IGuiGame gui = controller.getGui(); + if (gui != null) { + GameView gameView = gui.getGameView(); + if (gameView != null && !gameView.isGameOver()) { + PlayerView priorityPlayer = findPriorityPlayer(gameView); + // Show the waiting message if priority player exists and is different from our player + PlayerView localPlayer = controller.getLocalPlayerView(); + if (priorityPlayer != null && (localPlayer == null || priorityPlayer.getId() != localPlayer.getId())) { + playerName = priorityPlayer.getName(); + } + } + } + } + + // Build the waiting message with player name and elapsed time + if (playerName != null) { + String timeStr = getElapsedTimeString(); + if (timeStr != null) { + return localizer.getMessage("lblWaitingForPlayerWithTime", playerName, timeStr); + } + return localizer.getMessage("lblWaitingForPlayer", playerName); + } + } + + // Default message for local games or when player info not available + return localizer.getMessage("lblWaitingforActions"); + } + + /** + * Get elapsed time as a formatted string (e.g., "5s", "1:23"). + * Returns null if wait just started (less than 2 seconds). + */ + private String getElapsedTimeString() { + if (waitStartTime == 0) { + return null; + } + long elapsedMs = System.currentTimeMillis() - waitStartTime; + long elapsedSec = elapsedMs / 1000; + + // Don't show timer for very short waits + if (elapsedSec < 2) { + return null; + } + + if (elapsedSec < 60) { + return elapsedSec + "s"; + } else { + long minutes = elapsedSec / 60; + long seconds = elapsedSec % 60; + return String.format("%d:%02d", minutes, seconds); + } + } + + /** + * Find the player with priority from the GameView. + * Checks PlayerView.getHasPriority() for each player. + * Falls back to getPlayerTurn() during game setup when no priority is set. + */ + private PlayerView findPriorityPlayer(GameView gameView) { + if (gameView.getPlayers() != null) { + for (PlayerView pv : gameView.getPlayers()) { + if (pv.getHasPriority()) { + return pv; + } + } + } + // Fallback to player turn during game setup (mulligan phase) + return gameView.getPlayerTurn(); + } + protected final boolean isActive() { return inputQueue.getInput() == this; } From f6d6d4f8bf1bb310bdbc7cccbe4c4932082d0776 Mon Sep 17 00:00:00 2001 From: MostCromulent Date: Sat, 7 Feb 2026 05:51:28 +1030 Subject: [PATCH 5/7] Reapply: Refactor waiting timer to AbstractGuiGame (PR 9671 feedback) Reapplies 9cfa358 which was reverted in 2afb0470. Consolidates timer logic into the existing awaitNextInput() mechanism in AbstractGuiGame instead of separate timer systems in InputLockUI and CMatchUI. - Revert InputLockUI to simple lockUI input (remove ~100 lines) - Revert CMatchUI.showPromptMessage to simple pass-through (remove ~185 lines) - Enhance AbstractGuiGame.updatePromptForAwait() with player name lookup, elapsed time display, and 1-second reschedule for network games - Remove unused lblYieldingWaitingForPlayer from en-US.properties Co-Authored-By: Claude Opus 4.6 --- .../java/forge/screens/match/CMatchUI.java | 192 +----------------- forge-gui/res/languages/en-US.properties | 1 - .../gamemodes/match/AbstractGuiGame.java | 93 ++++++++- .../gamemodes/match/input/InputLockUI.java | 115 +---------- 4 files changed, 94 insertions(+), 307 deletions(-) diff --git a/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java b/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java index bdda4d1fd5c..eb0c753b8be 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java +++ b/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java @@ -176,12 +176,6 @@ public final class CMatchUI private final CStack cStack = new CStack(this); private int nextNotifiableStackIndex = 0; - // Timer for "Waiting for..." messages in network games - // Fields accessed from Swing Timer callbacks must be volatile for thread safety - private javax.swing.Timer waitingTimer; - private volatile long waitingStartTime; - private volatile String waitingBaseMessage; - public CMatchUI() { this.view = new VMatchUI(this); this.screen = FScreen.getMatchScreen(this, view); @@ -825,7 +819,6 @@ public void enableOverlay() { @Override public void finishGame() { - stopWaitingTimer(); // Clean up waiting timer FloatingZone.closeAll(); //ensure floating card areas cleared and closed after the game final GameView gameView = getGameView(); if (hasLocalPlayers() || gameView.isMatchOver()) { @@ -970,190 +963,7 @@ public void popupMenuCanceled(PopupMenuEvent e) { @Override public void showPromptMessage(final PlayerView playerView, final String message) { - // First, enhance generic waiting messages with player name (for host) - String enhancedMessage = enhanceWaitingMessageForHost(message, playerView); - - // Handle timer for "Waiting for..." messages in network games - if (GuiBase.isNetworkplay() && enhancedMessage != null && isWaitingMessage(enhancedMessage)) { - // Check if this is the same base message (just timer update) or a new waiting message - String baseMsg = extractBaseWaitingMessage(enhancedMessage); - if (!baseMsg.equals(waitingBaseMessage)) { - // New waiting message - start timer - stopWaitingTimer(); - waitingBaseMessage = baseMsg; - waitingStartTime = System.currentTimeMillis(); - startWaitingTimer(); - } - // Display message with current timer - cPrompt.setMessage(getWaitingMessageWithTimer()); - } else { - // Not a waiting message - stop timer and show message directly - stopWaitingTimer(); - waitingBaseMessage = null; - cPrompt.setMessage(enhancedMessage); - } - } - - /** - * Enhance generic "Waiting for opponent" and "Yielding" messages with the actual player name. - * This is used on the host side where messages aren't sent through NetGuiGame. - */ - private String enhanceWaitingMessageForHost(final String message, final PlayerView forPlayer) { - if (!GuiBase.isNetworkplay() || message == null || message.isEmpty()) { - return message; - } - - Localizer localizer = Localizer.getInstance(); - String waitingForOpponent = localizer.getMessage("lblWaitingForOpponent"); - String yieldingMessage = localizer.getMessage("lblYieldingUntilEndOfTurn"); - - boolean isWaitingOpponent = message.equals(waitingForOpponent); - boolean isYielding = message.equals(yieldingMessage); - - if (!isWaitingOpponent && !isYielding) { - return message; - } - - // Get the priority player from the Game object - GameView gameView = getGameView(); - if (gameView == null) { - return message; - } - - // Find the player we're waiting for - String waitingForName = findWaitingForPlayerName(gameView, forPlayer); - if (waitingForName == null) { - return message; - } - - // Return enhanced message with player name - if (isYielding) { - return localizer.getMessage("lblYieldingWaitingForPlayer", waitingForName); - } else { - return localizer.getMessage("lblWaitingForPlayer", waitingForName); - } - } - - /** - * Find the name of the player we're waiting for. - * Uses GameView data only to work correctly on both host and client. - * Checks priority player, turn player, or falls back to finding other players. - */ - private String findWaitingForPlayerName(GameView gameView, PlayerView forPlayer) { - if (gameView.isGameOver()) { - return null; - } - - // First, try to find the player with priority from GameView - PlayerView priorityPlayer = findPriorityPlayer(gameView); - - // If we found a priority player different from forPlayer, use them - if (priorityPlayer != null && (forPlayer == null || priorityPlayer.getId() != forPlayer.getId())) { - return priorityPlayer.getName(); - } - - // During mulligan or game setup, priority may not be set - // Fall back to finding any non-local player (in network games, that's who we're waiting for) - if (gameView.getPlayers() != null) { - for (PlayerView pv : gameView.getPlayers()) { - // Skip forPlayer if specified - if (forPlayer != null && pv.getId() == forPlayer.getId()) { - continue; - } - // Skip local players - we want the remote player we're waiting for - if (!isLocalPlayer(pv)) { - return pv.getName(); - } - } - } - - return null; - } - - /** - * Find the player with priority from the GameView. - * Checks PlayerView.getHasPriority() for each player. - * Falls back to getPlayerTurn() during game setup when no priority is set. - */ - private PlayerView findPriorityPlayer(GameView gameView) { - if (gameView.getPlayers() != null) { - for (PlayerView pv : gameView.getPlayers()) { - if (pv.getHasPriority()) { - return pv; - } - } - } - // Fallback to player turn during game setup (mulligan phase) - return gameView.getPlayerTurn(); - } - - private boolean isWaitingMessage(String message) { - Localizer localizer = Localizer.getInstance(); - String waitingForOpponent = localizer.getMessage("lblWaitingForOpponent"); - String waitingForActions = localizer.getMessage("lblWaitingforActions"); - - // Extract the localized prefix from "Waiting for {0}..." to match player-specific messages - // This avoids hardcoding English strings and works with any locale - String waitingForPlayerTemplate = localizer.getMessage("lblWaitingForPlayer", ""); - // The template with empty string gives us the prefix before the player name - // e.g., "Waiting for ..." -> we use "Waiting for " as prefix - String localizedWaitingPrefix = waitingForPlayerTemplate.replace("...", "").trim(); - - // Also get the yielding+waiting pattern for completeness - String yieldingTemplate = localizer.getMessage("lblYieldingWaitingForPlayer", ""); - - return message.equals(waitingForOpponent) - || message.equals(waitingForActions) - || message.startsWith(localizedWaitingPrefix) - || message.contains(localizedWaitingPrefix) - || message.startsWith(yieldingTemplate.split("\\{")[0]); - } - - private String extractBaseWaitingMessage(String message) { - // Remove any existing timer suffix like " (5s)" or " (1:23)" - return message.replaceAll(" \\(\\d+s\\)$", "").replaceAll(" \\(\\d+:\\d{2}\\)$", ""); - } - - private String getWaitingMessageWithTimer() { - if (waitingBaseMessage == null) { - return ""; - } - long elapsedMs = System.currentTimeMillis() - waitingStartTime; - long elapsedSec = elapsedMs / 1000; - - // Don't show timer for very short waits - if (elapsedSec < 2) { - return waitingBaseMessage; - } - - String timeStr; - if (elapsedSec < 60) { - timeStr = elapsedSec + "s"; - } else { - long minutes = elapsedSec / 60; - long seconds = elapsedSec % 60; - timeStr = String.format("%d:%02d", minutes, seconds); - } - return waitingBaseMessage + " (" + timeStr + ")"; - } - - private void startWaitingTimer() { - if (waitingTimer != null) { - return; // Already running - } - waitingTimer = new javax.swing.Timer(1000, e -> { - if (waitingBaseMessage != null) { - cPrompt.setMessage(getWaitingMessageWithTimer()); - } - }); - waitingTimer.start(); - } - - private void stopWaitingTimer() { - if (waitingTimer != null) { - waitingTimer.stop(); - waitingTimer = null; - } + cPrompt.setMessage(message); } @Override diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index c380b7bd403..c78a42aab4e 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -1532,7 +1532,6 @@ lblCloseGameSpectator=This will close this game and you will not be able to resu lblCloseGame=Close Game? lblWaitingForOpponent=Waiting for opponent... lblYieldingUntilEndOfTurn=Yielding until end of turn.\nYou may cancel this yield to take an action. -lblYieldingWaitingForPlayer=Yielding until end of turn. Waiting for {0}...\nCancel to take an action. lblStopWatching=Stop Watching lblEnterNumberBetweenMinAndMax=Enter a number between {0} and {1}: lblEnterNumberGreaterThanOrEqualsToMin=Enter a number greater than or equal to {0}: diff --git a/forge-gui/src/main/java/forge/gamemodes/match/AbstractGuiGame.java b/forge-gui/src/main/java/forge/gamemodes/match/AbstractGuiGame.java index 333981e1311..68b2fc91ca7 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/AbstractGuiGame.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/AbstractGuiGame.java @@ -446,10 +446,12 @@ public final boolean mayAutoPass(final PlayerView player) { private Timer awaitNextInputTimer; private TimerTask awaitNextInputTask; + private volatile long awaitStartTime; @Override public final void awaitNextInput() { checkAwaitNextInputTimer(); + awaitStartTime = System.currentTimeMillis(); //delay updating prompt to await next input briefly so buttons don't flicker disabled then enabled awaitNextInputTask = new TimerTask() { @Override @@ -459,7 +461,12 @@ public void run() { synchronized (awaitNextInputTimer) { if (awaitNextInputTask != null) { updatePromptForAwait(getCurrentPlayer()); - awaitNextInputTask = null; + // In network games, reschedule every second to update elapsed time + if (GuiBase.isNetworkplay()) { + scheduleTimerUpdate(); + } else { + awaitNextInputTask = null; + } } } }); @@ -467,6 +474,29 @@ public void run() { }; awaitNextInputTimer.schedule(awaitNextInputTask, 250); } + + private void scheduleTimerUpdate() { + awaitNextInputTask = new TimerTask() { + @Override + public void run() { + FThreads.invokeInEdtLater(() -> { + checkAwaitNextInputTimer(); + synchronized (awaitNextInputTimer) { + if (awaitNextInputTask != null) { + showPromptMessage(getCurrentPlayer(), getWaitingMessage(getCurrentPlayer())); + scheduleTimerUpdate(); + } + } + }); + } + }; + try { + awaitNextInputTimer.schedule(awaitNextInputTask, 1000); + } catch (final IllegalStateException ex) { + // Timer was cancelled between check and schedule + } + } + private void checkAwaitNextInputTimer() { if (awaitNextInputTimer == null) { String name = "?"; @@ -477,10 +507,68 @@ private void checkAwaitNextInputTimer() { } protected final void updatePromptForAwait(final PlayerView playerView) { - showPromptMessage(playerView, Localizer.getInstance().getMessage("lblWaitingForOpponent")); + showPromptMessage(playerView, getWaitingMessage(playerView)); updateButtons(playerView, false, false, false); } + private String getWaitingMessage(final PlayerView forPlayer) { + Localizer localizer = Localizer.getInstance(); + + if (GuiBase.isNetworkplay() && gameView != null && !gameView.isGameOver()) { + String name = findWaitingForPlayerName(forPlayer); + if (name != null) { + String timeStr = getElapsedTimeString(); + if (timeStr != null) { + return localizer.getMessage("lblWaitingForPlayerWithTime", name, timeStr); + } + return localizer.getMessage("lblWaitingForPlayer", name); + } + } + + return localizer.getMessage("lblWaitingForOpponent"); + } + + private String findWaitingForPlayerName(final PlayerView forPlayer) { + if (gameView.getPlayers() != null) { + for (PlayerView pv : gameView.getPlayers()) { + if (pv.getHasPriority() && (forPlayer == null || pv.getId() != forPlayer.getId())) { + return pv.getName(); + } + } + } + // Fallback to turn player during mulligan/setup + PlayerView turnPlayer = gameView.getPlayerTurn(); + if (turnPlayer != null && (forPlayer == null || turnPlayer.getId() != forPlayer.getId())) { + return turnPlayer.getName(); + } + // Fallback to any non-local player + if (gameView.getPlayers() != null) { + for (PlayerView pv : gameView.getPlayers()) { + if (forPlayer != null && pv.getId() == forPlayer.getId()) { + continue; + } + if (!isLocalPlayer(pv)) { + return pv.getName(); + } + } + } + return null; + } + + private String getElapsedTimeString() { + if (awaitStartTime == 0) { + return null; + } + long elapsedSec = (System.currentTimeMillis() - awaitStartTime) / 1000; + if (elapsedSec < 2) { + return null; + } + if (elapsedSec < 60) { + return elapsedSec + "s"; + } + return String.format("%d:%02d", elapsedSec / 60, elapsedSec % 60); + } + @Override public final void cancelAwaitNextInput() { if (awaitNextInputTimer == null) { @@ -495,6 +583,7 @@ public final void cancelAwaitNextInput() { awaitNextInputTask = null; } } + awaitStartTime = 0; } @Override diff --git a/forge-gui/src/main/java/forge/gamemodes/match/input/InputLockUI.java b/forge-gui/src/main/java/forge/gamemodes/match/input/InputLockUI.java index 296aae8b4b6..be1a4600bcb 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/input/InputLockUI.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/input/InputLockUI.java @@ -1,15 +1,10 @@ package forge.gamemodes.match.input; -import forge.game.Game; -import forge.game.GameView; import forge.game.card.Card; -import forge.game.phase.PhaseHandler; import forge.game.player.Player; import forge.game.player.PlayerView; import forge.game.spellability.SpellAbility; import forge.gui.FThreads; -import forge.gui.GuiBase; -import forge.gui.interfaces.IGuiGame; import forge.player.PlayerControllerHuman; import forge.util.ITriggerEvent; import forge.util.Localizer; @@ -20,7 +15,6 @@ public class InputLockUI implements Input { private final AtomicInteger iCall = new AtomicInteger(); - private volatile long waitStartTime = 0; private final InputQueue inputQueue; private final PlayerControllerHuman controller; @@ -37,7 +31,6 @@ public PlayerView getOwner() { @Override public void showMessageInitial() { final int ixCall = 1 + iCall.getAndIncrement(); - waitStartTime = System.currentTimeMillis(); ThreadUtil.delay(500, new InputUpdater(ixCall)); } @@ -55,14 +48,10 @@ public InputUpdater(final int idxCall) { @Override public void run() { - if (ixCall != iCall.get() || !isActive()) { + if ( ixCall != iCall.get() || !isActive()) { return; } FThreads.invokeInEdtLater(showMessageFromEdt); - // Reschedule to update timer display every second (only in network games) - if (GuiBase.isNetworkplay()) { - ThreadUtil.delay(1000, this); - } } } @@ -70,110 +59,10 @@ public void run() { @Override public void run() { controller.getGui().updateButtons(InputLockUI.this.getOwner(), "", "", false, false, false); - showMessage(getWaitingMessage()); + showMessage(Localizer.getInstance().getMessage("lblWaitingforActions")); } }; - /** - * Get a descriptive waiting message. - * In network games, shows which player we're waiting for with elapsed time. - * In local games, shows the generic "Waiting for Actions" message. - */ - private String getWaitingMessage() { - Localizer localizer = Localizer.getInstance(); - - // In network games, show who we're waiting for - if (GuiBase.isNetworkplay()) { - String playerName = null; - - // First try: Get priority player from the local Game object (works on host) - Player player = controller.getPlayer(); - if (player != null) { - Game game = player.getGame(); - if (game != null && !game.isGameOver()) { - PhaseHandler ph = game.getPhaseHandler(); - if (ph != null) { - Player priorityPlayer = ph.getPriorityPlayer(); - if (priorityPlayer != null && priorityPlayer != player) { - playerName = priorityPlayer.getName(); - } - } - } - } - - // Fallback: Get priority player from the GameView (works on client) - // On the network client, the Game object is on the server, but GameView is synced - if (playerName == null) { - IGuiGame gui = controller.getGui(); - if (gui != null) { - GameView gameView = gui.getGameView(); - if (gameView != null && !gameView.isGameOver()) { - PlayerView priorityPlayer = findPriorityPlayer(gameView); - // Show the waiting message if priority player exists and is different from our player - PlayerView localPlayer = controller.getLocalPlayerView(); - if (priorityPlayer != null && (localPlayer == null || priorityPlayer.getId() != localPlayer.getId())) { - playerName = priorityPlayer.getName(); - } - } - } - } - - // Build the waiting message with player name and elapsed time - if (playerName != null) { - String timeStr = getElapsedTimeString(); - if (timeStr != null) { - return localizer.getMessage("lblWaitingForPlayerWithTime", playerName, timeStr); - } - return localizer.getMessage("lblWaitingForPlayer", playerName); - } - } - - // Default message for local games or when player info not available - return localizer.getMessage("lblWaitingforActions"); - } - - /** - * Get elapsed time as a formatted string (e.g., "5s", "1:23"). - * Returns null if wait just started (less than 2 seconds). - */ - private String getElapsedTimeString() { - if (waitStartTime == 0) { - return null; - } - long elapsedMs = System.currentTimeMillis() - waitStartTime; - long elapsedSec = elapsedMs / 1000; - - // Don't show timer for very short waits - if (elapsedSec < 2) { - return null; - } - - if (elapsedSec < 60) { - return elapsedSec + "s"; - } else { - long minutes = elapsedSec / 60; - long seconds = elapsedSec % 60; - return String.format("%d:%02d", minutes, seconds); - } - } - - /** - * Find the player with priority from the GameView. - * Checks PlayerView.getHasPriority() for each player. - * Falls back to getPlayerTurn() during game setup when no priority is set. - */ - private PlayerView findPriorityPlayer(GameView gameView) { - if (gameView.getPlayers() != null) { - for (PlayerView pv : gameView.getPlayers()) { - if (pv.getHasPriority()) { - return pv; - } - } - } - // Fallback to player turn during game setup (mulligan phase) - return gameView.getPlayerTurn(); - } - protected final boolean isActive() { return inputQueue.getInput() == this; } From 277d89c6794aacf98ae7abd530a6bb88a1dc4e1d Mon Sep 17 00:00:00 2001 From: MostCromulent Date: Sat, 7 Feb 2026 14:44:09 +1030 Subject: [PATCH 6/7] Add lobby chat enhancements extracted from NetworkPlay/main These server-side chat features were missed during the earlier extraction to NetworkPlay/chat (since merged to master). Moving here as UI-focused. - Broadcast ready state with player count when toggling ready - Show (Host) indicator next to host's chat messages - Distinguish host login ('Lobby hosted by') vs player join ('joined the lobby') - Detect and announce player name changes - Fix duplicate message display by removing MessageEvent from LobbyInputHandler - Use 'left the lobby' instead of 'left the room' for consistency Co-Authored-By: Claude Opus 4.6 --- .../gamemodes/net/server/FServerManager.java | 69 ++++++++++++++++--- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/forge-gui/src/main/java/forge/gamemodes/net/server/FServerManager.java b/forge-gui/src/main/java/forge/gamemodes/net/server/FServerManager.java index ea4852fc088..ab61840833c 100644 --- a/forge-gui/src/main/java/forge/gamemodes/net/server/FServerManager.java +++ b/forge-gui/src/main/java/forge/gamemodes/net/server/FServerManager.java @@ -234,6 +234,34 @@ public void updateLobbyState() { public void updateSlot(final int index, final UpdateLobbyPlayerEvent event) { localLobby.applyToSlot(index, event); + + if (event.getReady() != null) { + broadcastReadyState(localLobby.getSlot(index).getName(), event.getReady()); + } + } + + private void broadcastReadyState(String playerName, boolean isReady) { + int readyCount = 0; + int totalPlayers = 0; + for (int i = 0; i < localLobby.getNumberOfSlots(); i++) { + LobbySlot slot = localLobby.getSlot(i); + if (slot.getType() == LobbySlotType.LOCAL || slot.getType() == LobbySlotType.REMOTE) { + totalPlayers++; + if (slot.isReady()) { + readyCount++; + } + } + } + if (isReady) { + broadcast(new MessageEvent(String.format("%s is ready (%d/%d players ready)", + playerName, readyCount, totalPlayers))); + if (readyCount == totalPlayers && totalPlayers > 1) { + broadcast(new MessageEvent("All players ready to start game!")); + } + } else { + broadcast(new MessageEvent(String.format("%s is not ready (%d/%d players ready)", + playerName, readyCount, totalPlayers))); + } } public IGuiGame getGui(final int index) { @@ -339,7 +367,14 @@ private class MessageHandler extends ChannelInboundHandlerAdapter { public final void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception { final RemoteClient client = clients.get(ctx.channel()); if (msg instanceof MessageEvent) { - broadcast(new MessageEvent(client.getUsername(), ((MessageEvent) msg).getMessage())); + String username = client.getUsername(); + String message = ((MessageEvent) msg).getMessage(); + + // Append (Host) indicator for the host player + if (client.getIndex() == 0) { + username = username + " (Host)"; + } + broadcast(new MessageEvent(username, message)); } super.channelRead(ctx, msg); } @@ -359,12 +394,31 @@ public void channelActive(final ChannelHandlerContext ctx) throws Exception { public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception { final RemoteClient client = clients.get(ctx.channel()); if (msg instanceof LoginEvent) { - final String username = ((LoginEvent) msg).getUsername(); + final LoginEvent event = (LoginEvent) msg; + final String username = event.getUsername(); client.setUsername(username); - broadcast(new MessageEvent(String.format("%s joined the room", username))); + if (client.getIndex() == 0) { + broadcast(new MessageEvent(String.format("Lobby hosted by %s", username))); + } else { + broadcast(new MessageEvent(String.format("%s joined the lobby", username))); + } updateLobbyState(); } else if (msg instanceof UpdateLobbyPlayerEvent) { - localLobby.applyToSlot(client.getIndex(), (UpdateLobbyPlayerEvent) msg); + UpdateLobbyPlayerEvent updateEvent = (UpdateLobbyPlayerEvent) msg; + localLobby.applyToSlot(client.getIndex(), updateEvent); + if (updateEvent.getName() != null) { + String oldName = client.getUsername(); + String newName = updateEvent.getName(); + if (!newName.equals(oldName)) { + client.setUsername(newName); + broadcast(new MessageEvent(String.format("%s changed their name to %s", oldName, newName))); + } + } + if (updateEvent.getReady() != null) { + broadcastReadyState(client.getUsername(), updateEvent.getReady()); + } + // Return to prevent duplicate processing by LobbyInputHandler + return; } super.channelRead(ctx, msg); } @@ -386,10 +440,9 @@ public void channelRead(final ChannelHandlerContext ctx, final Object msg) throw } } else if (msg instanceof UpdateLobbyPlayerEvent) { updateSlot(client.getIndex(), (UpdateLobbyPlayerEvent) msg); - } else if (msg instanceof MessageEvent) { - final MessageEvent event = (MessageEvent) msg; - lobbyListener.message(event.getSource(), event.getMessage()); } + // Note: MessageEvent is handled by MessageHandler, not here + // to avoid duplicate display on host's chat super.channelRead(ctx, msg); } } @@ -400,7 +453,7 @@ public void channelInactive(final ChannelHandlerContext ctx) throws Exception { final RemoteClient client = clients.remove(ctx.channel()); final String username = client.getUsername(); localLobby.disconnectPlayer(client.getIndex()); - broadcast(new MessageEvent(String.format("%s left the room", username))); + broadcast(new MessageEvent(String.format("%s left the lobby", username))); broadcast(new LogoutEvent(username)); super.channelInactive(ctx); } From 7b2e2543150cb96a6fe279d32ce1daf0bf8de790 Mon Sep 17 00:00:00 2001 From: MostCromulent Date: Sat, 7 Feb 2026 19:20:55 +1030 Subject: [PATCH 7/7] Refactor GuiBase.isNetworkplay to support per-instance query (PR 9671 feedback) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an overloaded isNetworkplay(IGuiGame) that checks whether a specific game instance is networked, rather than relying solely on the global static boolean which reflects whichever lobby last called setNetworkplay(). This follows tool4ever's proposed API from PR #9671. New API surface: - IGuiGame.isNetGame(): default method returning false. Only NetGuiGame overrides this to return true, so no other implementors need changes. - IGuiBase.hasNetGame(): implemented by GuiDesktop and GuiMobile, both delegating to the existing static flag. This serves as the fallback when no IGuiGame reference is available. - GuiBase.isNetworkplay(IGuiGame): if a game reference is provided, queries game.isNetGame() directly; otherwise falls back to getInterface().hasNetGame(). The existing no-arg isNetworkplay(), the static field, and setNetworkplay() are all unchanged. Migrated call sites (10 total) — these pass their available IGuiGame: - AbstractGuiGame: 2 sites use AbstractGuiGame.this / this - FControlGameEventHandler: 1 site uses matchController field - PlayerControllerHuman: 3 sites use getGui() - InputBase: 2 sites use controller.getGui() - MatchController (mobile): 5 sites use this Unmigrated call sites (no-arg fallback, no IGuiGame in scope): - FThreads (utility class, no game context) - VCardDisplayArea, MatchScreen (mobile views, no direct IGuiGame ref) Co-Authored-By: Claude Opus 4.6 --- forge-gui-desktop/src/main/java/forge/GuiDesktop.java | 6 ++++++ forge-gui-mobile/src/forge/GuiMobile.java | 5 +++++ .../src/forge/screens/match/MatchController.java | 10 +++++----- .../java/forge/gamemodes/match/AbstractGuiGame.java | 4 ++-- .../java/forge/gamemodes/match/input/InputBase.java | 4 ++-- .../java/forge/gamemodes/net/server/NetGuiGame.java | 3 +++ forge-gui/src/main/java/forge/gui/GuiBase.java | 10 ++++++++++ .../forge/gui/control/FControlGameEventHandler.java | 2 +- .../src/main/java/forge/gui/interfaces/IGuiBase.java | 3 +++ .../src/main/java/forge/gui/interfaces/IGuiGame.java | 3 +++ .../main/java/forge/player/PlayerControllerHuman.java | 6 +++--- 11 files changed, 43 insertions(+), 13 deletions(-) diff --git a/forge-gui-desktop/src/main/java/forge/GuiDesktop.java b/forge-gui-desktop/src/main/java/forge/GuiDesktop.java index f6ae7f95d4b..b71cdd47306 100644 --- a/forge-gui-desktop/src/main/java/forge/GuiDesktop.java +++ b/forge-gui-desktop/src/main/java/forge/GuiDesktop.java @@ -33,6 +33,7 @@ import forge.gui.CardListChooser; import forge.gui.CardListViewer; import forge.gui.FThreads; +import forge.gui.GuiBase; import forge.gui.GuiChoose; import forge.gui.download.GuiDownloadService; import forge.gui.framework.FScreen; @@ -368,6 +369,11 @@ private static float initializeScreenScale() { } static float screenScale = initializeScreenScale(); + @Override + public boolean hasNetGame() { + return GuiBase.isNetworkplay(); // fallback to existing static flag + } + @Override public float getScreenScale() { return screenScale; diff --git a/forge-gui-mobile/src/forge/GuiMobile.java b/forge-gui-mobile/src/forge/GuiMobile.java index 3ebac46e967..4acf6f415b9 100644 --- a/forge-gui-mobile/src/forge/GuiMobile.java +++ b/forge-gui-mobile/src/forge/GuiMobile.java @@ -362,6 +362,11 @@ public void preventSystemSleep(boolean preventSleep) { Forge.getDeviceAdapter().preventSystemSleep(preventSleep); } + @Override + public boolean hasNetGame() { + return GuiBase.isNetworkplay(); + } + @Override public float getScreenScale() { return 1f; diff --git a/forge-gui-mobile/src/forge/screens/match/MatchController.java b/forge-gui-mobile/src/forge/screens/match/MatchController.java index 94e8b9ed986..af9740d2832 100644 --- a/forge-gui-mobile/src/forge/screens/match/MatchController.java +++ b/forge-gui-mobile/src/forge/screens/match/MatchController.java @@ -143,7 +143,7 @@ public void refreshCardDetails(final Iterable cards) { @Override public void refreshField() { - if(!GuiBase.isNetworkplay()) + if(!GuiBase.isNetworkplay(this)) return; refreshCardDetails(null); } @@ -182,7 +182,7 @@ public void openView(final TrackableCollection myPlayers) { } } view = new MatchScreen(playerPanels); - if(GuiBase.isNetworkplay()) + if(GuiBase.isNetworkplay(this)) view.resetFields(); clearSelectables(); //fix uncleared selection @@ -251,19 +251,19 @@ public void updatePhase(boolean saveState) { final VPhaseIndicator.PhaseLabel phaseLabel = view.getPlayerPanel(lastPlayer).getPhaseIndicator().getLabel(ph); if (phaseLabel != null) phaseLabel.setActive(true); - if (GuiBase.isNetworkplay()) + if (GuiBase.isNetworkplay(this)) getGameView().updateNeedsPhaseRedrawn(lastPlayer, PhaseType.CLEANUP); } else if (getGameView().getPlayerTurn() != null) { //set phaselabel final VPhaseIndicator.PhaseLabel phaseLabel = view.getPlayerPanel(getGameView().getPlayerTurn()).getPhaseIndicator().getLabel(ph); if (phaseLabel != null) phaseLabel.setActive(true); - if (GuiBase.isNetworkplay()) + if (GuiBase.isNetworkplay(this)) getGameView().updateNeedsPhaseRedrawn(getGameView().getPlayerTurn(), ph); } } - if(GuiBase.isNetworkplay()) + if(GuiBase.isNetworkplay(this)) checkStack(); if (ph != null && saveState && ph.isMain()) { diff --git a/forge-gui/src/main/java/forge/gamemodes/match/AbstractGuiGame.java b/forge-gui/src/main/java/forge/gamemodes/match/AbstractGuiGame.java index 68b2fc91ca7..59e8cf32a50 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/AbstractGuiGame.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/AbstractGuiGame.java @@ -462,7 +462,7 @@ public void run() { if (awaitNextInputTask != null) { updatePromptForAwait(getCurrentPlayer()); // In network games, reschedule every second to update elapsed time - if (GuiBase.isNetworkplay()) { + if (GuiBase.isNetworkplay(AbstractGuiGame.this)) { scheduleTimerUpdate(); } else { awaitNextInputTask = null; @@ -514,7 +514,7 @@ protected final void updatePromptForAwait(final PlayerView playerView) { private String getWaitingMessage(final PlayerView forPlayer) { Localizer localizer = Localizer.getInstance(); - if (GuiBase.isNetworkplay() && gameView != null && !gameView.isGameOver()) { + if (GuiBase.isNetworkplay(this) && gameView != null && !gameView.isGameOver()) { String name = findWaitingForPlayerName(forPlayer); if (name != null) { String timeStr = getElapsedTimeString(); diff --git a/forge-gui/src/main/java/forge/gamemodes/match/input/InputBase.java b/forge-gui/src/main/java/forge/gamemodes/match/input/InputBase.java index f59d1ee0d1a..3cb79709b7d 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/input/InputBase.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/input/InputBase.java @@ -124,13 +124,13 @@ protected final void showMessage(final String message) { controller.getGui().showPromptMessage(getOwner(), message); } protected final void showMessage(final String message, final SpellAbilityView sav) { - if (GuiBase.isNetworkplay()) //todo additional check to pass this + if (GuiBase.isNetworkplay(controller.getGui())) //todo additional check to pass this controller.getGui().showPromptMessage(getOwner(), message); else controller.getGui().showCardPromptMessage(getOwner(), message, sav.getHostCard()); } protected final void showMessage(final String message, final CardView card) { - if (GuiBase.isNetworkplay()) //todo additional check to pass this + if (GuiBase.isNetworkplay(controller.getGui())) //todo additional check to pass this controller.getGui().showPromptMessage(getOwner(), message); else controller.getGui().showCardPromptMessage(getOwner(), message, card); diff --git a/forge-gui/src/main/java/forge/gamemodes/net/server/NetGuiGame.java b/forge-gui/src/main/java/forge/gamemodes/net/server/NetGuiGame.java index 0a0c7b54963..3ef59f0f582 100644 --- a/forge-gui/src/main/java/forge/gamemodes/net/server/NetGuiGame.java +++ b/forge-gui/src/main/java/forge/gamemodes/net/server/NetGuiGame.java @@ -314,6 +314,9 @@ public boolean isUiSetToSkipPhase(final PlayerView playerTurn, final PhaseType p return sendAndWait(ProtocolMethod.isUiSetToSkipPhase, playerTurn, phase); } + @Override + public boolean isNetGame() { return true; } + @Override protected void updateCurrentPlayer(final PlayerView player) { // TODO Auto-generated method stub diff --git a/forge-gui/src/main/java/forge/gui/GuiBase.java b/forge-gui/src/main/java/forge/gui/GuiBase.java index 4007becd4b2..263e6d8c5cf 100644 --- a/forge-gui/src/main/java/forge/gui/GuiBase.java +++ b/forge-gui/src/main/java/forge/gui/GuiBase.java @@ -2,6 +2,7 @@ import forge.util.HWInfo; import forge.gui.interfaces.IGuiBase; +import forge.gui.interfaces.IGuiGame; import forge.localinstance.properties.ForgePreferences; public class GuiBase { @@ -61,6 +62,15 @@ public static String getDownloadsDir() { public static int getDeviceRAM() { return deviceRAM; } public static boolean isNetworkplay() { return networkplay; } + public static boolean isNetworkplay(IGuiGame game) { + if (game != null) { + // query AbstractGuiGame implementation if provided + return game.isNetGame(); + } + // both IGuiBase implementations should have (at least indirect) access to matches + // to check all available IGuiGame + return getInterface().hasNetGame(); + } public static void setNetworkplay(boolean value) { networkplay = value; } public static boolean hasPropertyConfig() { return propertyConfig; } diff --git a/forge-gui/src/main/java/forge/gui/control/FControlGameEventHandler.java b/forge-gui/src/main/java/forge/gui/control/FControlGameEventHandler.java index d0319ca3135..86c79c4033f 100644 --- a/forge-gui/src/main/java/forge/gui/control/FControlGameEventHandler.java +++ b/forge-gui/src/main/java/forge/gui/control/FControlGameEventHandler.java @@ -393,7 +393,7 @@ public Void visit(final GameEventCombatEnded event) { @Override public Void visit(final GameEventCombatUpdate event) { - if (!GuiBase.isNetworkplay()) + if (!GuiBase.isNetworkplay(matchController)) return null; //not needed if single player only... final CardCollection cards = new CardCollection(); diff --git a/forge-gui/src/main/java/forge/gui/interfaces/IGuiBase.java b/forge-gui/src/main/java/forge/gui/interfaces/IGuiBase.java index fcad3f9218a..c71f2376b08 100644 --- a/forge-gui/src/main/java/forge/gui/interfaces/IGuiBase.java +++ b/forge-gui/src/main/java/forge/gui/interfaces/IGuiBase.java @@ -65,4 +65,7 @@ public interface IGuiBase { void preventSystemSleep(boolean preventSleep); float getScreenScale(); UpnpServiceConfiguration getUpnpPlatformService(); + + /** Returns true if any currently active game is a network game. */ + boolean hasNetGame(); } \ No newline at end of file diff --git a/forge-gui/src/main/java/forge/gui/interfaces/IGuiGame.java b/forge-gui/src/main/java/forge/gui/interfaces/IGuiGame.java index b56468dcc52..66ff5d8c280 100644 --- a/forge-gui/src/main/java/forge/gui/interfaces/IGuiGame.java +++ b/forge-gui/src/main/java/forge/gui/interfaces/IGuiGame.java @@ -278,4 +278,7 @@ public interface IGuiGame { void clearAutoYields(); void setCurrentPlayer(PlayerView player); + + /** Returns true if this game instance is a network game. */ + default boolean isNetGame() { return false; } } diff --git a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java index 7aef50cf68e..a9d0d28701e 100644 --- a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java +++ b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java @@ -1002,7 +1002,7 @@ public ImmutablePair arrangeForScry(final CardCo tempShowCards(topN); if (FModel.getPreferences().getPrefBoolean(FPref.UI_SELECT_FROM_CARD_DISPLAYS) && - (!GuiBase.getInterface().isLibgdxPort()) && (!GuiBase.isNetworkplay())) { //prevent crash for desktop vs mobile port it will crash the netplay since mobile doesnt have manipulatecardlist, send the alternate below + (!GuiBase.getInterface().isLibgdxPort()) && (!GuiBase.isNetworkplay(getGui()))) { //prevent crash for desktop vs mobile port it will crash the netplay since mobile doesnt have manipulatecardlist, send the alternate below CardCollectionView cardList = player.getCardsIn(ZoneType.Library); ImmutablePair result = arrangeForMove(localizer.getMessage("lblMoveCardstoToporBbottomofLibrary"), cardList, topN, true, true); @@ -1942,7 +1942,7 @@ public ReplacementEffect chooseSingleReplacementEffect(final List re.getId() == rev.getId()).findAny().orElse(first); @@ -1963,7 +1963,7 @@ public StaticAbility chooseSingleStaticAbility(final String prompt, final List st.getId() == stv.getId()).findAny().orElse(first);