From 35d85b657450a0d3046f64a638da46e52b88600d Mon Sep 17 00:00:00 2001 From: MostCromulent <201167372+MostCromulent@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:24:01 +1030 Subject: [PATCH 1/4] Split online lobby into separate Host and Join buttons Replace the single lobby entry point with dedicated Host and Join buttons on both desktop and mobile. Desktop gets an info panel with warning text and a link to the Network Play guide. Mobile uses the preferred-screen pattern for navigation. NetConnectUtil is refactored to separate host setup (ensurePlayerName) from join setup (getJoinServerUrl). Co-Authored-By: Claude Opus 4.6 --- .../home/online/CSubmenuOnlineLobby.java | 36 +++---- .../forge/screens/home/online/OnlineMenu.java | 15 ++- .../home/online/VSubmenuOnlineLobby.java | 84 +++++++++++++++- .../src/forge/screens/home/HomeScreen.java | 6 +- .../screens/online/OnlineLobbyScreen.java | 97 +++++++++++-------- .../src/forge/screens/online/OnlineMenu.java | 17 ++-- forge-gui/res/languages/en-US.properties | 6 ++ .../forge/gamemodes/net/NetConnectUtil.java | 21 +++- 8 files changed, 194 insertions(+), 88 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 2fcb1ded4cb..5d5c5d426aa 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 @@ -37,30 +37,30 @@ void setLobby(final VLobby lobbyView) { initialize(); } - void connectToServer() { - final String url = NetConnectUtil.getServerUrl(); - if (url == null) { return; } - + void hostGame() { + NetConnectUtil.ensurePlayerName(); FThreads.invokeInBackgroundThread(() -> { - if (!url.isEmpty()) { - join(url); - } - else { - try { - host(); - } catch (Exception ex) { - // IntelliJ swears that BindException isn't thrown in this try block, but it is! - if (ex.getClass() == BindException.class) { - SOptionPane.showErrorDialog(Localizer.getInstance().getMessage("lblUnableStartServerPortAlreadyUse")); - SOverlayUtils.hideOverlay(); - } else { - BugReporter.reportException(ex); - } + try { + host(); + } catch (Exception ex) { + // IntelliJ swears that BindException isn't thrown in this try block, but it is! + if (ex.getClass() == BindException.class) { + SOptionPane.showErrorDialog(Localizer.getInstance().getMessage("lblUnableStartServerPortAlreadyUse")); + SOverlayUtils.hideOverlay(); + } else { + BugReporter.reportException(ex); } } }); } + void joinGame() { + final String url = NetConnectUtil.getJoinServerUrl(); + if (url == null) { return; } + + FThreads.invokeInBackgroundThread(() -> join(url)); + } + private void host() { SwingUtilities.invokeLater(() -> { SOverlayUtils.startGameOverlay(Localizer.getInstance().getMessage("lblStartingServer")); diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/online/OnlineMenu.java b/forge-gui-desktop/src/main/java/forge/screens/home/online/OnlineMenu.java index e36ccd85f1a..a2141c084d0 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/online/OnlineMenu.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/online/OnlineMenu.java @@ -18,7 +18,8 @@ public final class OnlineMenu { public static JMenu getMenu() { JMenu menu = new JMenu(Localizer.getInstance().getMessage("lblOnline")); menu.setMnemonic(KeyEvent.VK_O); - menu.add(getMenuItem_ConnectToServer()); + menu.add(getMenuItem_HostGame()); + menu.add(getMenuItem_JoinGame()); menu.add(new JSeparator()); menu.add(chatItem); return menu; @@ -38,9 +39,15 @@ public static JMenu getMenu() { }); } - private static JMenuItem getMenuItem_ConnectToServer() { - JMenuItem menuItem = new JMenuItem(Localizer.getInstance().getMessage("lblConnectToServer")); - menuItem.addActionListener(e -> CSubmenuOnlineLobby.SINGLETON_INSTANCE.connectToServer()); + private static JMenuItem getMenuItem_HostGame() { + JMenuItem menuItem = new JMenuItem(Localizer.getInstance().getMessage("lblHostGame")); + menuItem.addActionListener(e -> CSubmenuOnlineLobby.SINGLETON_INSTANCE.hostGame()); + return menuItem; + } + + private static JMenuItem getMenuItem_JoinGame() { + JMenuItem menuItem = new JMenuItem(Localizer.getInstance().getMessage("lblJoinGame")); + menuItem.addActionListener(e -> CSubmenuOnlineLobby.SINGLETON_INSTANCE.joinGame()); return menuItem; } } \ No newline at end of file diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/online/VSubmenuOnlineLobby.java b/forge-gui-desktop/src/main/java/forge/screens/home/online/VSubmenuOnlineLobby.java index c08110b3608..aa03dc4841b 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/online/VSubmenuOnlineLobby.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/online/VSubmenuOnlineLobby.java @@ -1,6 +1,14 @@ package forge.screens.home.online; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Font; + +import javax.swing.BorderFactory; +import javax.swing.JEditorPane; import javax.swing.JPanel; +import javax.swing.SwingConstants; +import javax.swing.event.HyperlinkEvent; import forge.deckchooser.FDeckChooser; import forge.gamemodes.match.GameLobby; @@ -21,6 +29,7 @@ import forge.screens.home.VHomeUI; import forge.screens.home.VLobby; import forge.toolbox.FButton; +import forge.toolbox.FLabel; import forge.toolbox.FSkin; import forge.util.Localizer; import net.miginfocom.swing.MigLayout; @@ -64,11 +73,76 @@ public void populate() { container.removeAll(); if (lobby == null) { - final FButton btnConnect = new FButton(Localizer.getInstance().getMessage("lblConnectToServer")); - btnConnect.setFont(FSkin.getRelativeFont(20)); - btnConnect.addActionListener(e -> getLayoutControl().connectToServer()); - container.setLayout(new MigLayout("insets 0, gap 0, ax center, ay center")); - container.add(btnConnect, "w 300!, h 75!"); + final Localizer localizer = Localizer.getInstance(); + final String guideUrl = "https://github.com/Card-Forge/forge/wiki/network-play"; + + // Bordered info box + final JPanel infoBox = new JPanel(new MigLayout("insets 30 40 20 40, gap 0, wrap 1, ax center")); + infoBox.setBackground(new Color(40, 40, 40)); + infoBox.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(100, 100, 100), 1), + BorderFactory.createEmptyBorder(10, 10, 10, 10))); + + // Title + final FLabel lblTitle = new FLabel.Builder() + .text("- = * H E R E B E E L D R A Z I * = -") + .fontSize(22).fontAlign(SwingConstants.CENTER).build(); + + // Warning text + final FLabel lblWarning = new FLabel.Builder() + .text(localizer.getMessage("lblOnlineWarning")) + .fontSize(16).fontAlign(SwingConstants.CENTER).build(); + + // Guide text with inline hyperlink (matches FLabel font and color) + final Font skinFont = FSkin.getRelativeFont(16).getBaseFont(); + final Color textColor = lblWarning.getForeground(); + final String hexColor = String.format("#%02x%02x%02x", + textColor.getRed(), textColor.getGreen(), textColor.getBlue()); + final JEditorPane guidePane = new JEditorPane("text/html", ""); + guidePane.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE); + guidePane.setFont(skinFont); + guidePane.setEditable(false); + guidePane.setOpaque(false); + guidePane.setText("
" + + localizer.getMessage("lblOnlineGuideText") + + " " + + localizer.getMessage("lblNetworkPlayGuide") + "." + + "
"); + guidePane.addHyperlinkListener(e -> { + if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { + try { + java.awt.Desktop.getDesktop().browse(e.getURL().toURI()); + } catch (final Exception ex) { + java.awt.Toolkit.getDefaultToolkit().getSystemClipboard() + .setContents(new java.awt.datatransfer.StringSelection(guideUrl), null); + } + } + }); + + // Buttons + final FButton btnHost = new FButton(localizer.getMessage("lblHostGame")); + btnHost.setFont(FSkin.getRelativeFont(18)); + btnHost.addActionListener(e -> getLayoutControl().hostGame()); + + final FButton btnJoin = new FButton(localizer.getMessage("lblJoinGame")); + btnJoin.setFont(FSkin.getRelativeFont(18)); + btnJoin.addActionListener(e -> getLayoutControl().joinGame()); + + final JPanel buttonPanel = new JPanel(new MigLayout("insets 0, gap 20, ax center")); + buttonPanel.setOpaque(false); + buttonPanel.add(btnHost, "w 200!, h 50!"); + buttonPanel.add(btnJoin, "w 200!, h 50!"); + + infoBox.add(lblTitle, "ax center, gap 0 0 0 15"); + infoBox.add(lblWarning, "ax center, gap 0 0 0 15"); + infoBox.add(guidePane, "ax center, growx, gap 0 0 0 25"); + infoBox.add(buttonPanel, "ax center"); + + container.setLayout(new BorderLayout()); + final JPanel wrapper = new JPanel(new MigLayout("ax center, ay center")); + wrapper.setOpaque(false); + wrapper.add(infoBox); + container.add(wrapper, BorderLayout.CENTER); if (container.isShowing()) { container.validate(); diff --git a/forge-gui-mobile/src/forge/screens/home/HomeScreen.java b/forge-gui-mobile/src/forge/screens/home/HomeScreen.java index 4fcd3a21f4f..b50fb156cd6 100644 --- a/forge-gui-mobile/src/forge/screens/home/HomeScreen.java +++ b/forge-gui-mobile/src/forge/screens/home/HomeScreen.java @@ -19,7 +19,7 @@ import forge.gui.FThreads; import forge.screens.FScreen; import forge.screens.achievements.AchievementsScreen; -import forge.screens.online.OnlineMenu.OnlineScreen; +import forge.screens.online.OnlineMenu; import forge.screens.planarconquest.ConquestMenu; import forge.screens.quest.QuestMenu; import forge.screens.settings.SettingsScreen; @@ -82,7 +82,7 @@ private HomeScreen() { addButton(Forge.getLocalizer().getMessage("lblPlayOnline"), e -> { activeButtonIndex = 2; Forge.lastButtonIndex = activeButtonIndex; - OnlineScreen.Lobby.open(); + OnlineMenu.getPreferredScreen().open(); }); addButton(Forge.getLocalizer().getMessage("lblDeckManager"), e -> { activeButtonIndex = 3; @@ -152,7 +152,7 @@ public void openMenu(int index){ if (index < 0) return; //menu on startup if (index == 2) - OnlineScreen.Lobby.open(); + OnlineMenu.getPreferredScreen().open(); else if (index < 6) NewGameMenu.getPreferredScreen().open(); else if (index == 6) diff --git a/forge-gui-mobile/src/forge/screens/online/OnlineLobbyScreen.java b/forge-gui-mobile/src/forge/screens/online/OnlineLobbyScreen.java index de05337d85a..b0cce932523 100644 --- a/forge-gui-mobile/src/forge/screens/online/OnlineLobbyScreen.java +++ b/forge-gui-mobile/src/forge/screens/online/OnlineLobbyScreen.java @@ -26,6 +26,12 @@ public OnlineLobbyScreen() { super(null, OnlineMenu.getMenu(), new OfflineLobby()); } + private static boolean hostMode = true; + + public static void setHostMode(boolean host) { + hostMode = host; + } + private static GameLobby gameLobby; public static GameLobby getGameLobby() { @@ -89,53 +95,58 @@ public void onActivate() { } if (getGameLobby() == null) { setGameLobby(getLobby()); - //prompt to connect to server when offline lobby activated + if (hostMode) { + activateHost(); + } else { + activateJoin(); + } + } + } + + private void activateHost() { + NetConnectUtil.ensurePlayerName(); + final String caption = Forge.getLocalizer().getMessage("lblStartingServer"); + LoadingOverlay.show(caption, true, () -> { + final ChatMessage[] result = new ChatMessage[1]; + final IOnlineChatInterface chatInterface = (IOnlineChatInterface) OnlineScreen.Chat.getScreen(); FThreads.invokeInBackgroundThread(() -> { - //Not in gui thread - final String url = NetConnectUtil.getServerUrl(); - FThreads.invokeInEdtLater(() -> { - //In GUI thread - if (url == null) { - closeConn(""); //go back to previous screen if user cancels connection + result[0] = NetConnectUtil.host(OnlineLobbyScreen.this, chatInterface); + chatInterface.addMessage(result[0]); + NetConnectUtil.copyHostedServerUrl(); + }); + OnlineScreen.Host.update(); + }); + } + + private void activateJoin() { + FThreads.invokeInBackgroundThread(() -> { + final String url = NetConnectUtil.getJoinServerUrl(); + FThreads.invokeInEdtLater(() -> { + if (url == null) { + closeConn(""); + return; + } + final String caption = Forge.getLocalizer().getMessage("lblConnectingToServer"); + LoadingOverlay.show(caption, true, () -> { + final ChatMessage[] result = new ChatMessage[1]; + final IOnlineChatInterface chatInterface = (IOnlineChatInterface) OnlineScreen.Chat.getScreen(); + result[0] = NetConnectUtil.join(url, OnlineLobbyScreen.this, chatInterface); + String message = result[0].getMessage(); + if (ForgeConstants.CLOSE_CONN_COMMAND.equals(message)) { + closeConn(Forge.getLocalizer().getMessage("UnableConnectToServer", url)); + return; + } else if (message != null && message.startsWith(ForgeConstants.CONN_ERROR_PREFIX)) { + 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; } - final boolean joinServer = url.length() > 0; - final String caption = joinServer ? Forge.getLocalizer().getMessage("lblConnectingToServer") : Forge.getLocalizer().getMessage("lblStartingServer"); - LoadingOverlay.show(caption, true, () -> { - //in GUI thread due to above LoadingOverlay.show call executing ThreadUtil.invokeInGameThread(() -> FThreads.invokeInEdtLater(() -> { runnable.run(); ... - final ChatMessage[] result = new ChatMessage[1]; - final IOnlineChatInterface chatInterface = (IOnlineChatInterface)OnlineScreen.Chat.getScreen(); - if (joinServer) { - result[0] = NetConnectUtil.join(url, OnlineLobbyScreen.this, chatInterface); - 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 (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; - } - chatInterface.addMessage(result[0]); - } - else { - FThreads.invokeInBackgroundThread( - () -> { - result[0] = NetConnectUtil.host(OnlineLobbyScreen.this, chatInterface); - chatInterface.addMessage(result[0]); - NetConnectUtil.copyHostedServerUrl(); - } - ); - } - //update menu buttons - OnlineScreen.Lobby.update(); - }); + chatInterface.addMessage(result[0]); + OnlineScreen.Host.update(); }); }); - } + }); } } diff --git a/forge-gui-mobile/src/forge/screens/online/OnlineMenu.java b/forge-gui-mobile/src/forge/screens/online/OnlineMenu.java index ca767d2b423..df7a48d063c 100644 --- a/forge-gui-mobile/src/forge/screens/online/OnlineMenu.java +++ b/forge-gui-mobile/src/forge/screens/online/OnlineMenu.java @@ -16,7 +16,8 @@ public class OnlineMenu extends FPopupMenu { public enum OnlineScreen { - Lobby("lblLobby", FSkinImage.FAVICON, OnlineLobbyScreen.class), + Host("lblHostGame", FSkinImage.FAVICON, OnlineLobbyScreen.class), + Join("lblJoinGame", FSkinImage.FAVICON, OnlineLobbyScreen.class), Chat("lblChat", FSkinImage.QUEST_NOTES, OnlineChatScreen.class), Disconnect("lblDisconnect", FSkinImage.DELETE, null); @@ -69,6 +70,9 @@ private void initializeScreen() { } public void open() { + if (this == Host || this == Join) { + OnlineLobbyScreen.setHostMode(this == Host); + } initializeScreen(); Forge.openScreen(screen); } @@ -84,14 +88,7 @@ public FScreen getScreen() { } public void update(){ - for (OnlineScreen ngs : OnlineScreen.values()) { - if (ngs.ordinal() == 2){ //disconect - if (getGameLobby() == null) - ngs.item.setEnabled(false); - else - ngs.item.setEnabled(true); - } - } + Disconnect.item.setEnabled(getGameLobby() != null); } } @@ -105,7 +102,7 @@ public void update(){ } catch (Exception ex) { ex.printStackTrace(); - preferredScreen = OnlineScreen.Lobby; + preferredScreen = OnlineScreen.Host; prefs.setPref(FPref.PLAY_ONLINE_SCREEN, preferredScreen.name()); prefs.save(); } diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index c04b3fd307d..59d0bbcbca2 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -3064,6 +3064,12 @@ lblStartingServer=Starting server... lblConnectingToServer=Connecting to server... #NetConnectUtil.java lblOnlineMultiplayerDest=This feature is under active development.\nYou are likely to find bugs.\n\n - = * H E R E B E E L D R A Z I * = -\n\nEnter the URL of the server to join.\nLeave blank to host your own server. +lblHostGame=Host a Game +lblJoinGame=Join a Game +lblEnterServerAddress=Enter the server address (e.g. 192.168.1.50:36743) +lblOnlineWarning=Online multiplayer is under active development. You are likely to find bugs. +lblOnlineGuideText=For setup instructions and troubleshooting, see the +lblNetworkPlayGuide=Network Play User Guide lblHostingPortOnN=Hosting on port {0}. lblShareURLToMakePlayerJoinServer=Share the following URL with anyone who wishes to join your server. It has been copied to your clipboard for convenience.\n\n{0}\n\nFor internal games, use the following URL: {1} lblForgeUnableDetermineYourExternalIP=Forge was unable to determine your external IP!\n\n{0} 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 e9116980d11..fdf7eb3b1c7 100644 --- a/forge-gui/src/main/java/forge/gamemodes/net/NetConnectUtil.java +++ b/forge-gui/src/main/java/forge/gamemodes/net/NetConnectUtil.java @@ -29,15 +29,26 @@ public class NetConnectUtil { private NetConnectUtil() { } - public static String getServerUrl() { - final String url = SOptionPane.showInputDialog(Localizer.getInstance().getMessage("lblOnlineMultiplayerDest"), Localizer.getInstance().getMessage("lblConnectToServer")); - if (url == null) { return null; } + /** + * Prompt for the server address to join. Returns null if cancelled, or the address string. + */ + public static String getJoinServerUrl() { + final String url = SOptionPane.showInputDialog( + Localizer.getInstance().getMessage("lblEnterServerAddress"), + Localizer.getInstance().getMessage("lblJoinGame")); + if (url == null || url.isEmpty()) { return null; } + + ensurePlayerName(); + return url; + } - //prompt user for player one name if needed + /** + * Ensure the player name is set before connecting. + */ + public static void ensurePlayerName() { if (StringUtils.isBlank(FModel.getPreferences().getPref(FPref.PLAYER_NAME))) { GamePlayerUtil.setPlayerName(); } - return url; } public static ChatMessage host(final IOnlineLobby onlineLobby, final IOnlineChatInterface chatInterface) { From 54458786997a48e89bc6535a6f612b0f7c373819 Mon Sep 17 00:00:00 2001 From: MostCromulent <201167372+MostCromulent@users.noreply.github.com> Date: Tue, 24 Feb 2026 07:53:42 +1030 Subject: [PATCH 2/4] Add mobile online play landing page with Host/Join buttons Mirror the desktop landing page on mobile: show warning text, guide link, and separate Host/Join buttons before any connection is made. Previously, tapping "Play Online" immediately started hosting with no choice, and switching to Join skipped the IP address dialog. - Replace Host/Join menu entries with single Lobby entry in OnlineMenu - Add landing page components to OnlineLobbyScreen (title, warning, guide link, Host/Join buttons) with proper show/hide on connect - Replace desktop JEditorPane hyperlink with FLabel for single-click - Add setLobbyControlsVisible() to LobbyScreen for landing/lobby toggle Co-Authored-By: Claude Opus 4.6 --- .../home/online/VSubmenuOnlineLobby.java | 47 +++---- .../screens/constructed/LobbyScreen.java | 10 ++ .../screens/online/OnlineLobbyScreen.java | 129 ++++++++++++++++-- .../src/forge/screens/online/OnlineMenu.java | 8 +- forge-gui/res/languages/en-US.properties | 2 +- 5 files changed, 148 insertions(+), 48 deletions(-) diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/online/VSubmenuOnlineLobby.java b/forge-gui-desktop/src/main/java/forge/screens/home/online/VSubmenuOnlineLobby.java index aa03dc4841b..6539f99bf9a 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/online/VSubmenuOnlineLobby.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/online/VSubmenuOnlineLobby.java @@ -5,10 +5,8 @@ import java.awt.Font; import javax.swing.BorderFactory; -import javax.swing.JEditorPane; import javax.swing.JPanel; import javax.swing.SwingConstants; -import javax.swing.event.HyperlinkEvent; import forge.deckchooser.FDeckChooser; import forge.gamemodes.match.GameLobby; @@ -93,31 +91,23 @@ public void populate() { .text(localizer.getMessage("lblOnlineWarning")) .fontSize(16).fontAlign(SwingConstants.CENTER).build(); - // Guide text with inline hyperlink (matches FLabel font and color) - final Font skinFont = FSkin.getRelativeFont(16).getBaseFont(); - final Color textColor = lblWarning.getForeground(); - final String hexColor = String.format("#%02x%02x%02x", - textColor.getRed(), textColor.getGreen(), textColor.getBlue()); - final JEditorPane guidePane = new JEditorPane("text/html", ""); - guidePane.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE); - guidePane.setFont(skinFont); - guidePane.setEditable(false); - guidePane.setOpaque(false); - guidePane.setText("
" - + localizer.getMessage("lblOnlineGuideText") - + " " - + localizer.getMessage("lblNetworkPlayGuide") + "." - + "
"); - guidePane.addHyperlinkListener(e -> { - if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { - try { - java.awt.Desktop.getDesktop().browse(e.getURL().toURI()); - } catch (final Exception ex) { - java.awt.Toolkit.getDefaultToolkit().getSystemClipboard() - .setContents(new java.awt.datatransfer.StringSelection(guideUrl), null); - } - } - }); + // Guide text with clickable link + final FLabel lblGuideText = new FLabel.Builder() + .text(localizer.getMessage("lblOnlineGuideText")) + .fontSize(16).fontAlign(SwingConstants.CENTER).build(); + + final FLabel lblGuideLink = new FLabel.Builder() + .text("" + localizer.getMessage("lblNetworkPlayGuide") + "") + .fontSize(16).fontStyle(Font.BOLD).fontAlign(SwingConstants.CENTER) + .hoverable().cmdClick(() -> { + try { + java.awt.Desktop.getDesktop().browse(java.net.URI.create(guideUrl)); + } catch (final Exception ex) { + java.awt.Toolkit.getDefaultToolkit().getSystemClipboard() + .setContents(new java.awt.datatransfer.StringSelection(guideUrl), null); + } + }).build(); + lblGuideLink.setCursor(java.awt.Cursor.getPredefinedCursor(java.awt.Cursor.HAND_CURSOR)); // Buttons final FButton btnHost = new FButton(localizer.getMessage("lblHostGame")); @@ -135,7 +125,8 @@ public void populate() { infoBox.add(lblTitle, "ax center, gap 0 0 0 15"); infoBox.add(lblWarning, "ax center, gap 0 0 0 15"); - infoBox.add(guidePane, "ax center, growx, gap 0 0 0 25"); + infoBox.add(lblGuideText, "ax center, gap 0 0 0 0"); + infoBox.add(lblGuideLink, "ax center, gap 0 0 0 25"); infoBox.add(buttonPanel, "ax center"); container.setLayout(new BorderLayout()); diff --git a/forge-gui-mobile/src/forge/screens/constructed/LobbyScreen.java b/forge-gui-mobile/src/forge/screens/constructed/LobbyScreen.java index 326129719d2..bfec5c64ed8 100644 --- a/forge-gui-mobile/src/forge/screens/constructed/LobbyScreen.java +++ b/forge-gui-mobile/src/forge/screens/constructed/LobbyScreen.java @@ -885,6 +885,16 @@ public FScrollPane getPlayersScroll() { return playersScroll; } + protected void setLobbyControlsVisible(boolean visible) { + lblPlayers.setVisible(visible); + cbPlayerCount.setVisible(visible); + lblVariants.setVisible(visible); + cbVariants.setVisible(visible); + lblGamesInMatch.setVisible(visible); + cbGamesInMatch.setVisible(visible); + playersScroll.setVisible(visible); + } + public void setStartButtonAvailability() { if (lobby.isAllowNetworking() && FServerManager.getInstance() != null) btnStart.setVisible(FServerManager.getInstance().isHosting()); diff --git a/forge-gui-mobile/src/forge/screens/online/OnlineLobbyScreen.java b/forge-gui-mobile/src/forge/screens/online/OnlineLobbyScreen.java index b0cce932523..9bb38222c7d 100644 --- a/forge-gui-mobile/src/forge/screens/online/OnlineLobbyScreen.java +++ b/forge-gui-mobile/src/forge/screens/online/OnlineLobbyScreen.java @@ -1,5 +1,6 @@ package forge.screens.online; +import com.badlogic.gdx.utils.Align; import com.google.common.collect.ImmutableList; import forge.Forge; @@ -20,16 +21,57 @@ import forge.screens.LoadingOverlay; import forge.screens.constructed.LobbyScreen; import forge.screens.online.OnlineMenu.OnlineScreen; +import forge.assets.FSkinColor; +import forge.assets.FSkinFont; +import forge.toolbox.FButton; +import forge.toolbox.FLabel; +import forge.util.Utils; public class OnlineLobbyScreen extends LobbyScreen implements IOnlineLobby { + + private static final String GUIDE_URL = "https://github.com/Card-Forge/forge/wiki/network-play"; + + // Landing page components + private final FLabel lblTitle; + private final FLabel lblWarning; + private final FLabel lblGuideText; + private final FLabel lblGuideLink; + private final FButton btnHost; + private final FButton btnJoin; + private boolean showLanding = true; + public OnlineLobbyScreen() { super(null, OnlineMenu.getMenu(), new OfflineLobby()); - } - - private static boolean hostMode = true; - public static void setHostMode(boolean host) { - hostMode = host; + lblTitle = new FLabel.Builder() + .text("- = * H E R E B E E L D R A Z I * = -") + .font(FSkinFont.get(18)).align(Align.center).build(); + add(lblTitle); + + lblWarning = new FLabel.Builder() + .text(Forge.getLocalizer().getMessage("lblOnlineWarning")) + .font(FSkinFont.get(14)).align(Align.center).build(); + add(lblWarning); + + lblGuideText = new FLabel.Builder() + .text(Forge.getLocalizer().getMessage("lblOnlineGuideText")) + .font(FSkinFont.get(12)).align(Align.center).build(); + add(lblGuideText); + + lblGuideLink = new FLabel.Builder() + .text(Forge.getLocalizer().getMessage("lblNetworkPlayGuide")) + .font(FSkinFont.get(14)).align(Align.center) + .textColor(FSkinColor.get(FSkinColor.Colors.CLR_ACTIVE)) + .command(e -> com.badlogic.gdx.Gdx.net.openURI(GUIDE_URL)).build(); + add(lblGuideLink); + + btnHost = new FButton(Forge.getLocalizer().getMessage("lblHostGame")); + btnHost.setCommand(e -> activateHost()); + add(btnHost); + + btnJoin = new FButton(Forge.getLocalizer().getMessage("lblJoinGame")); + btnJoin.setCommand(e -> activateJoin()); + add(btnJoin); } private static GameLobby gameLobby; @@ -94,16 +136,74 @@ public void onActivate() { return; } if (getGameLobby() == null) { - setGameLobby(getLobby()); - if (hostMode) { - activateHost(); - } else { - activateJoin(); - } + showLanding = true; + revalidate(); + } else { + showLanding = false; + super.onActivate(); + } + } + + @Override + protected void doLayoutAboveBtnStart(float startY, float width, float height) { + if (showLanding) { + // Hide lobby controls and start button during landing page + btnStart.setVisible(false); + setLobbyControlsVisible(false); + + float padding = Utils.scale(10); + float y = startY + height * 0.15f; + + // Title + float labelHeight = lblTitle.getAutoSizeBounds().height + padding; + lblTitle.setBounds(padding, y, width - 2 * padding, labelHeight); + lblTitle.setVisible(true); + y += labelHeight + padding * 2; + + // Warning + labelHeight = lblWarning.getAutoSizeBounds().height + padding; + lblWarning.setBounds(padding, y, width - 2 * padding, labelHeight); + lblWarning.setVisible(true); + y += labelHeight + padding * 2; + + // Guide text + labelHeight = lblGuideText.getAutoSizeBounds().height + padding; + lblGuideText.setBounds(padding, y, width - 2 * padding, labelHeight); + lblGuideText.setVisible(true); + y += labelHeight; + + // Guide link + labelHeight = lblGuideLink.getAutoSizeBounds().height + padding; + lblGuideLink.setBounds(padding, y, width - 2 * padding, labelHeight); + lblGuideLink.setVisible(true); + y += labelHeight + padding * 4; + + // Buttons side by side + float buttonWidth = (width - 3 * padding) / 2; + float buttonHeight = Utils.AVG_FINGER_HEIGHT; + btnHost.setBounds(padding, y, buttonWidth, buttonHeight); + btnHost.setVisible(true); + btnJoin.setBounds(padding * 2 + buttonWidth, y, buttonWidth, buttonHeight); + btnJoin.setVisible(true); + } else { + // Hide landing page components, show lobby controls + lblTitle.setVisible(false); + lblWarning.setVisible(false); + lblGuideText.setVisible(false); + lblGuideLink.setVisible(false); + btnHost.setVisible(false); + btnJoin.setVisible(false); + setLobbyControlsVisible(true); + btnStart.setVisible(true); + + super.doLayoutAboveBtnStart(startY, width, height); } } private void activateHost() { + showLanding = false; + setGameLobby(getLobby()); + revalidate(); NetConnectUtil.ensurePlayerName(); final String caption = Forge.getLocalizer().getMessage("lblStartingServer"); LoadingOverlay.show(caption, true, () -> { @@ -114,11 +214,14 @@ private void activateHost() { chatInterface.addMessage(result[0]); NetConnectUtil.copyHostedServerUrl(); }); - OnlineScreen.Host.update(); + OnlineScreen.Lobby.update(); }); } private void activateJoin() { + showLanding = false; + setGameLobby(getLobby()); + revalidate(); FThreads.invokeInBackgroundThread(() -> { final String url = NetConnectUtil.getJoinServerUrl(); FThreads.invokeInEdtLater(() -> { @@ -144,7 +247,7 @@ private void activateJoin() { return; } chatInterface.addMessage(result[0]); - OnlineScreen.Host.update(); + OnlineScreen.Lobby.update(); }); }); }); diff --git a/forge-gui-mobile/src/forge/screens/online/OnlineMenu.java b/forge-gui-mobile/src/forge/screens/online/OnlineMenu.java index df7a48d063c..205a54ba4a1 100644 --- a/forge-gui-mobile/src/forge/screens/online/OnlineMenu.java +++ b/forge-gui-mobile/src/forge/screens/online/OnlineMenu.java @@ -16,8 +16,7 @@ public class OnlineMenu extends FPopupMenu { public enum OnlineScreen { - Host("lblHostGame", FSkinImage.FAVICON, OnlineLobbyScreen.class), - Join("lblJoinGame", FSkinImage.FAVICON, OnlineLobbyScreen.class), + Lobby("lblPlayOnline", FSkinImage.FAVICON, OnlineLobbyScreen.class), Chat("lblChat", FSkinImage.QUEST_NOTES, OnlineChatScreen.class), Disconnect("lblDisconnect", FSkinImage.DELETE, null); @@ -70,9 +69,6 @@ private void initializeScreen() { } public void open() { - if (this == Host || this == Join) { - OnlineLobbyScreen.setHostMode(this == Host); - } initializeScreen(); Forge.openScreen(screen); } @@ -102,7 +98,7 @@ public void update(){ } catch (Exception ex) { ex.printStackTrace(); - preferredScreen = OnlineScreen.Host; + preferredScreen = OnlineScreen.Lobby; prefs.setPref(FPref.PLAY_ONLINE_SCREEN, preferredScreen.name()); prefs.save(); } diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index 59d0bbcbca2..71ebbf77840 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -3066,7 +3066,7 @@ lblConnectingToServer=Connecting to server... lblOnlineMultiplayerDest=This feature is under active development.\nYou are likely to find bugs.\n\n - = * H E R E B E E L D R A Z I * = -\n\nEnter the URL of the server to join.\nLeave blank to host your own server. lblHostGame=Host a Game lblJoinGame=Join a Game -lblEnterServerAddress=Enter the server address (e.g. 192.168.1.50:36743) +lblEnterServerAddress=Enter the server IP address and port (e.g. 192.168.1.50:36743) lblOnlineWarning=Online multiplayer is under active development. You are likely to find bugs. lblOnlineGuideText=For setup instructions and troubleshooting, see the lblNetworkPlayGuide=Network Play User Guide From efa466e96572ade42e78c5a430999306da904c8a Mon Sep 17 00:00:00 2001 From: MostCromulent <201167372+MostCromulent@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:56:19 +1030 Subject: [PATCH 3/4] Update Network-Play.md for Host/Join button changes Co-Authored-By: Claude Opus 4.6 --- docs/Network-Play.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/Network-Play.md b/docs/Network-Play.md index c930bce7452..be35c81ab8b 100644 --- a/docs/Network-Play.md +++ b/docs/Network-Play.md @@ -41,22 +41,22 @@ There is no built-in matchmaking. Network play is designed for playing against p 1. **Configure network** — Host must configure network settings to enable external connections (see [Network Configuration](#network-configuration) below). 2. **Verify versions** — Confirm all devices are running the same Forge version (see [Version Compatibility](#version-compatibility) below). -3. **Launch Forge** on all devices. +3. **Launch Forge** on all devices and navigate to the online play screen. - Mobile: Choose "Classic Mode", then "Play Online" - - Desktop: "Online Multiplayer" > "Lobby" > "Connect to Server" -4. **Host** leaves the server address field **empty** and clicks OK. + - Desktop: "Online Multiplayer" > "Lobby" +4. **Host** clicks **"Host a Game"** to start the server. - On first host, Forge will ask whether to **automatically open the port via UPnP** (see [UPnP](#upnp-automatic-port-forwarding) below). If your router supports UPnP, choosing "Just Once" or "Always" can skip manual port forwarding entirely. 5. **Host** determines address to share with clients: - **Local play:** Use the **Copy Server URL** button in the lobby — this copies the address in the correct format. Forge displays the host's IP (typically `192.168.x.x`). Verify against the device's network settings. Ignore any suggestion to use `localhost`. - **Remote play:** Verify the host's external IP at [canyouseeme.org](http://canyouseeme.org). - 7. **Client** enters the host's address in the connection dialog and clicks OK. +6. **Client** clicks **"Join a Game"** and enters the host's address. - The address format is **`IP:port`** — for example: `192.168.1.50:36743` (local) or `203.0.113.45:36743` (remote). - If the port is omitted, Forge defaults to 36743. -8. **Configure the match:** +7. **Configure the match:** - Host selects match type, teams, and game settings. - All players select decks, sleeves, and avatars. - Each player toggles their **Ready** switch. -9. **Host starts the match** once all players are ready. +8. **Host starts the match** once all players are ready. --- From 9a80acf49c370bef4e42d3084f0afa36347a9e3c Mon Sep 17 00:00:00 2001 From: MostCromulent <201167372+MostCromulent@users.noreply.github.com> Date: Sat, 28 Feb 2026 14:26:47 +1030 Subject: [PATCH 4/4] Adjust mobile landing page: match font sizes, reduce button width, tighten spacing Co-Authored-By: Claude Opus 4.6 --- .../forge/screens/online/OnlineLobbyScreen.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/forge-gui-mobile/src/forge/screens/online/OnlineLobbyScreen.java b/forge-gui-mobile/src/forge/screens/online/OnlineLobbyScreen.java index 9bb38222c7d..828b4e7d09e 100644 --- a/forge-gui-mobile/src/forge/screens/online/OnlineLobbyScreen.java +++ b/forge-gui-mobile/src/forge/screens/online/OnlineLobbyScreen.java @@ -55,7 +55,7 @@ public OnlineLobbyScreen() { lblGuideText = new FLabel.Builder() .text(Forge.getLocalizer().getMessage("lblOnlineGuideText")) - .font(FSkinFont.get(12)).align(Align.center).build(); + .font(FSkinFont.get(14)).align(Align.center).build(); add(lblGuideText); lblGuideLink = new FLabel.Builder() @@ -164,7 +164,7 @@ protected void doLayoutAboveBtnStart(float startY, float width, float height) { labelHeight = lblWarning.getAutoSizeBounds().height + padding; lblWarning.setBounds(padding, y, width - 2 * padding, labelHeight); lblWarning.setVisible(true); - y += labelHeight + padding * 2; + y += labelHeight + padding; // Guide text labelHeight = lblGuideText.getAutoSizeBounds().height + padding; @@ -178,12 +178,15 @@ protected void doLayoutAboveBtnStart(float startY, float width, float height) { lblGuideLink.setVisible(true); y += labelHeight + padding * 4; - // Buttons side by side - float buttonWidth = (width - 3 * padding) / 2; + // Buttons side by side, centered with margin + float buttonGap = padding * 2; + float buttonWidth = width * 0.35f; + float totalButtonWidth = buttonWidth * 2 + buttonGap; + float buttonX = (width - totalButtonWidth) / 2; float buttonHeight = Utils.AVG_FINGER_HEIGHT; - btnHost.setBounds(padding, y, buttonWidth, buttonHeight); + btnHost.setBounds(buttonX, y, buttonWidth, buttonHeight); btnHost.setVisible(true); - btnJoin.setBounds(padding * 2 + buttonWidth, y, buttonWidth, buttonHeight); + btnJoin.setBounds(buttonX + buttonWidth + buttonGap, y, buttonWidth, buttonHeight); btnJoin.setVisible(true); } else { // Hide landing page components, show lobby controls