From 64a360aeb6f50801f749a9ce5acde0c228ba4545 Mon Sep 17 00:00:00 2001 From: kenjitamura Date: Sun, 9 Aug 2026 10:57:50 -0600 Subject: [PATCH 1/6] Added command line modes to launch forge into the lobby as either a host or joiner --- .../home/online/CSubmenuOnlineLobby.java | 12 +++ .../src/main/java/forge/view/HostMatch.java | 80 +++++++++++++++++++ .../src/main/java/forge/view/JoinMatch.java | 59 ++++++++++++++ .../src/main/java/forge/view/Main.java | 23 ++++++ .../forge/gamemodes/net/NetConnectUtil.java | 22 ++++- .../gamemodes/net/server/FServerManager.java | 29 ++++++- 6 files changed, 220 insertions(+), 5 deletions(-) create mode 100644 forge-gui-desktop/src/main/java/forge/view/HostMatch.java create mode 100644 forge-gui-desktop/src/main/java/forge/view/JoinMatch.java 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 1170ddf8bee7..537b4bc12b03 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 @@ -77,6 +77,18 @@ void joinGame() { FThreads.invokeInBackgroundThread(() -> join(url)); } + /** + * Programmatic entry point to join a game hosted at the given server URL + * (e.g. "ipaddress:port"), used by the command-line "join" mode. + * Reuses the same join logic as {@link #joinGame()} but with the URL supplied + * directly instead of prompting for it. + */ + public void joinServer(final String url) { + if (url == null || url.isEmpty()) { 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/view/HostMatch.java b/forge-gui-desktop/src/main/java/forge/view/HostMatch.java new file mode 100644 index 000000000000..5450e195b776 --- /dev/null +++ b/forge-gui-desktop/src/main/java/forge/view/HostMatch.java @@ -0,0 +1,80 @@ +/* + * Forge: Play Magic: the Gathering. + * Copyright (C) 2011 Forge Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package forge.view; + +import javax.swing.SwingUtilities; + +import forge.gamemodes.net.ChatMessage; +import forge.gamemodes.net.NetConnectUtil; +import forge.gui.FNetOverlay; +import forge.gui.SOverlayUtils; +import forge.gui.framework.EDocID; +import forge.screens.home.CHomeUI; +import forge.screens.home.online.VSubmenuOnlineLobby; + +/** + * Command-line "host" mode launcher. + *

+ * Starts a local multiplayer server on the given port (e.g. {@code 36743}) for + * others to join. Port forwarding is assumed to already be in place, so the + * UPnP / port-forwarding prompt normally shown when hosting from the GUI is + * skipped entirely. + */ +public final class HostMatch { + private HostMatch() { + } + + /** + * Hosts a game on the given local {@code port}. + *

+ * This should be called after the Forge singletons and controller have + * been initialized (see {@link Main}). The hosting work is deferred to the + * EDT so it runs after the main window is shown, mirroring the normal GUI + * "Host A Game" flow but without the port-forwarding prompt. + * + * @param port the local port to bind the server to + */ + public static void host(final int port) { + SwingUtilities.invokeLater(() -> { + try { + System.out.println("[HostMatch] moving to network lobby screen..."); + CHomeUI.SINGLETON_INSTANCE.itemClick(EDocID.HOME_NETWORK); + System.out.println("[HostMatch] hosting game on port " + port + " (port forwarding assumed configured)..."); + + SOverlayUtils.startGameOverlay("Starting Server..."); + SOverlayUtils.showOverlay(); + + final ChatMessage result = NetConnectUtil.host( + VSubmenuOnlineLobby.SINGLETON_INSTANCE, + FNetOverlay.SINGLETON_INSTANCE, + port); + + SOverlayUtils.hideOverlay(); + FNetOverlay.SINGLETON_INSTANCE.show(result); + if (CHomeUI.SINGLETON_INSTANCE.getCurrentDocID() == EDocID.HOME_NETWORK) { + VSubmenuOnlineLobby.SINGLETON_INSTANCE.populate(); + } + System.out.println("[HostMatch] hosting started."); + } catch (final Throwable t) { + // Print directly so it always shows on the console (even though + // ExceptionHandler also duplicates it to forge.log). + t.printStackTrace(); + } + }); + } +} diff --git a/forge-gui-desktop/src/main/java/forge/view/JoinMatch.java b/forge-gui-desktop/src/main/java/forge/view/JoinMatch.java new file mode 100644 index 000000000000..766cb480e18b --- /dev/null +++ b/forge-gui-desktop/src/main/java/forge/view/JoinMatch.java @@ -0,0 +1,59 @@ +/* + * Forge: Play Magic: the Gathering. + * Copyright (C) 2011 Forge Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package forge.view; + +import forge.gui.FThreads; +import forge.gui.framework.EDocID; +import forge.screens.home.CHomeUI; +import forge.screens.home.online.CSubmenuOnlineLobby; + +/** + * Command-line "join" mode launcher. + *

+ * Brings the client up to the network lobby and connects it to a game hosted + * at the supplied server URL (e.g. {@code ipaddress:port}) using the same + * code path as pressing "Join A Game" in the GUI. + */ +public final class JoinMatch { + private JoinMatch() { + } + + /** + * Launches the GUI client into the lobby of the game hosted at + * {@code serverUrl} (e.g. {@code "127.0.0.1:26782"}). + *

+ * This should be called after the Forge singletons and controller have + * been initialized (see {@link Main}). It schedules the join on the EDT. + * + * @param serverUrl the {@code host:port} (or full URL) of the game server to join + */ + public static void join(final String serverUrl) { + if (serverUrl == null || serverUrl.isEmpty()) { + System.out.println("No server URL supplied. Usage: forge join ipaddress:port"); + return; + } + + // Navigate to and prepare the online lobby screen, then join the server. + // Done on the EDT since it manipulates Swing state and mirrors the + // normal "Join A Game" button flow. + FThreads.invokeInEdtLater(() -> { + CHomeUI.SINGLETON_INSTANCE.itemClick(EDocID.HOME_NETWORK); + CSubmenuOnlineLobby.SINGLETON_INSTANCE.joinServer(serverUrl); + }); + } +} diff --git a/forge-gui-desktop/src/main/java/forge/view/Main.java b/forge-gui-desktop/src/main/java/forge/view/Main.java index 8fb44a9cf6ef..9827a96b8368 100644 --- a/forge-gui-desktop/src/main/java/forge/view/Main.java +++ b/forge-gui-desktop/src/main/java/forge/view/Main.java @@ -85,6 +85,29 @@ public static void main(final String[] args) { System.out.println("Dedicated server mode.\nNot implemented."); break; + case "join": + Singletons.initializeOnce(true); + Singletons.getControl().initialize(); + JoinMatch.join(args.length > 1 ? args[1] : ""); + return; + + case "host": + Singletons.initializeOnce(true); + Singletons.getControl().initialize(); + + int port = forge.model.FModel.getNetPreferences() + .getPrefInt(forge.localinstance.properties.ForgeNetPreferences.FNetPref.NET_PORT); + if (args.length > 1) { + try { + port = Integer.parseInt(args[1].trim()); + } catch (final NumberFormatException e) { + System.out.println("Invalid port '" + args[1] + "'. Using default port " + port + "."); + } + } + + HostMatch.host(port); + return; + default: System.out.println("Unknown mode.\nKnown mode is 'sim', 'parse' "); break; 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 b3f15c37968d..e69ea54b8f27 100644 --- a/forge-gui/src/main/java/forge/gamemodes/net/NetConnectUtil.java +++ b/forge-gui/src/main/java/forge/gamemodes/net/NetConnectUtil.java @@ -54,12 +54,32 @@ public static void ensurePlayerName() { public static ChatMessage host(final IOnlineLobby onlineLobby, final IOnlineChatInterface chatInterface) { final int port = FModel.getNetPreferences().getPrefInt(ForgeNetPreferences.FNetPref.NET_PORT); + return host(onlineLobby, chatInterface, port, null); + } + + /** + * Hosts a game on the given local {@code port}. This is the command-line + * "host" entry point: it never prompts about port forwarding, on the + * assumption that forwarding is already configured externally. + * + * @param onlineLobby the online-lobby view/controller pairing + * @param chatInterface the chat interface used for server chat + * @param port the local port to bind the server to + * @return a {@link ChatMessage} describing the result (e.g. hosting on a port) + */ + public static ChatMessage host(final IOnlineLobby onlineLobby, final IOnlineChatInterface chatInterface, + final int port) { + return host(onlineLobby, chatInterface, port, false); + } + + private static ChatMessage host(final IOnlineLobby onlineLobby, final IOnlineChatInterface chatInterface, + final int port, final Boolean forceUPnP) { final FServerManager server = FServerManager.getInstance(); final ServerGameLobby lobby = new ServerGameLobby(); final ILobbyView view = onlineLobby.setLobby(lobby); NetworkLogConfig.activateNetworkLogging(); - server.startServer(port); + server.startServer(port, forceUPnP); server.setLobby(lobby); lobby.setListener(new IUpdateable() { 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 3642e6c9ee9e..d9f21f0e9e90 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 @@ -225,13 +225,34 @@ public forge.gamemodes.net.NetworkByteTracker getByteTracker() { } public void startServer(final int port) { + startServer(port, null); + } + + /** + * Starts the multiplayer server listening on the given {@code port}. + *

+ * If {@code forceUPnP} is non-{@code null} it overrides the stored + * preference and no port-forwarding dialog is shown; otherwise the stored + * {@code UPnP} preference is consulted (and may prompt the user). Passing + * {@link Boolean#FALSE} is used by command-line headless hosting, where the + * caller has already arranged port forwarding. + * + * @param port the local port to bind + * @param forceUPnP {@code true}/{@code false} to force UPnP handling, or + * {@code null} to use the stored preference + */ + public void startServer(final int port, final Boolean forceUPnP) { this.port = port; - String UPnPOption = FModel.getNetPreferences().getPref(ForgeNetPreferences.FNetPref.UPnP); boolean startUPnP; - if (UPnPOption.equalsIgnoreCase("ASK")) { - startUPnP = callUPnPDialog(); + if (forceUPnP != null) { + startUPnP = forceUPnP; } else { - startUPnP = UPnPOption.equalsIgnoreCase("ALWAYS"); + String UPnPOption = FModel.getNetPreferences().getPref(ForgeNetPreferences.FNetPref.UPnP); + if (UPnPOption.equalsIgnoreCase("ASK")) { + startUPnP = callUPnPDialog(); + } else { + startUPnP = UPnPOption.equalsIgnoreCase("ALWAYS"); + } } netLog.info("Starting Multiplayer Server"); try { From c5e0651c007d7f54e19ffdff5722deadb4fbc7af Mon Sep 17 00:00:00 2001 From: kenjitamura Date: Sun, 9 Aug 2026 11:57:32 -0600 Subject: [PATCH 2/6] Add documentation for command line launch to lobby --- docs/Network-Play.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/Network-Play.md b/docs/Network-Play.md index 0a0d593c8ff7..96889ab3d1d8 100644 --- a/docs/Network-Play.md +++ b/docs/Network-Play.md @@ -64,6 +64,18 @@ --- +# Command Line Launch +> [!TIP] +> A **Client** will fail to join if the **Host** hasn't added a player for them in advance +> The **Host** and **Client** will join with the last defined player name +> The first two steps of the [**Quick Start**](#quick-start) are still required +> Automatic UPnP isn't currently supported with command line arguments so the host must make sure to have configured port forwarding in advance + +1. **Host** adds to their launcher or calls the executable for their platform manually with the additional arguments: `host `**`port`** +2. **Client** adds to their launcher or calls the executable for their platform manually with the additional arguments: `join `**`IP:port`** + +--- + # Disconnect/Reconnect Support > [!IMPORTANT] From 65706fb909c3f897bc36dcc0b2d88ddc521ad0d4 Mon Sep 17 00:00:00 2001 From: kenjitamura Date: Sun, 9 Aug 2026 12:09:15 -0600 Subject: [PATCH 3/6] Update callouts for clarity and better aligned with the content --- docs/Network-Play.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/Network-Play.md b/docs/Network-Play.md index 96889ab3d1d8..ffdb474c4301 100644 --- a/docs/Network-Play.md +++ b/docs/Network-Play.md @@ -66,10 +66,11 @@ # Command Line Launch > [!TIP] -> A **Client** will fail to join if the **Host** hasn't added a player for them in advance -> The **Host** and **Client** will join with the last defined player name -> The first two steps of the [**Quick Start**](#quick-start) are still required -> Automatic UPnP isn't currently supported with command line arguments so the host must make sure to have configured port forwarding in advance +> - The first two steps of the [**Quick Start**](#quick-start) are still required +> - The **Host** and **Client** will join with the last defined player name +> [!CAUTION] +> - A **Client** will fail to join if the **Host** hasn't added a player for them in advance +> - Automatic UPnP isn't currently supported with command line arguments so the host must make sure to have configured port forwarding in advance 1. **Host** adds to their launcher or calls the executable for their platform manually with the additional arguments: `host `**`port`** 2. **Client** adds to their launcher or calls the executable for their platform manually with the additional arguments: `join `**`IP:port`** From 1f12b4bccb39798fed46f7f74339d3b4a932dfc6 Mon Sep 17 00:00:00 2001 From: kenjitamura Date: Sun, 9 Aug 2026 12:10:52 -0600 Subject: [PATCH 4/6] Add missing line break to separate callouts --- docs/Network-Play.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Network-Play.md b/docs/Network-Play.md index ffdb474c4301..679c0cba5c15 100644 --- a/docs/Network-Play.md +++ b/docs/Network-Play.md @@ -68,6 +68,7 @@ > [!TIP] > - The first two steps of the [**Quick Start**](#quick-start) are still required > - The **Host** and **Client** will join with the last defined player name + > [!CAUTION] > - A **Client** will fail to join if the **Host** hasn't added a player for them in advance > - Automatic UPnP isn't currently supported with command line arguments so the host must make sure to have configured port forwarding in advance From 3187ac1e8dde7e670ebbdc9095028e985ec9bb99 Mon Sep 17 00:00:00 2001 From: kenjitamura Date: Sun, 9 Aug 2026 12:27:02 -0600 Subject: [PATCH 5/6] Add host/join to known modes when invalid mode is specified --- forge-gui-desktop/src/main/java/forge/view/Main.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge-gui-desktop/src/main/java/forge/view/Main.java b/forge-gui-desktop/src/main/java/forge/view/Main.java index 9827a96b8368..bfd33e287229 100644 --- a/forge-gui-desktop/src/main/java/forge/view/Main.java +++ b/forge-gui-desktop/src/main/java/forge/view/Main.java @@ -109,7 +109,7 @@ public static void main(final String[] args) { return; default: - System.out.println("Unknown mode.\nKnown mode is 'sim', 'parse' "); + System.out.println("Unknown mode.\nKnown mode is 'sim', 'parse', 'host', 'join' "); break; } From 9c5172b68fbf6e272f74045c82773996d2c17295 Mon Sep 17 00:00:00 2001 From: kenjitamura Date: Sun, 9 Aug 2026 12:59:48 -0600 Subject: [PATCH 6/6] Added ensurePlayerName calls and tested by clearing the PLAYER_NAME from preferences before host and join launches --- docs/Network-Play.md | 2 +- forge-gui-desktop/src/main/java/forge/view/HostMatch.java | 1 + forge-gui-desktop/src/main/java/forge/view/JoinMatch.java | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/Network-Play.md b/docs/Network-Play.md index 679c0cba5c15..3561bdc422c0 100644 --- a/docs/Network-Play.md +++ b/docs/Network-Play.md @@ -67,7 +67,7 @@ # Command Line Launch > [!TIP] > - The first two steps of the [**Quick Start**](#quick-start) are still required -> - The **Host** and **Client** will join with the last defined player name +> - The **Host** and **Client** will join with the last defined player name from preferences or prompt for a player name if the value is empty e.g. first launch > [!CAUTION] > - A **Client** will fail to join if the **Host** hasn't added a player for them in advance diff --git a/forge-gui-desktop/src/main/java/forge/view/HostMatch.java b/forge-gui-desktop/src/main/java/forge/view/HostMatch.java index 5450e195b776..be7d7e56cac7 100644 --- a/forge-gui-desktop/src/main/java/forge/view/HostMatch.java +++ b/forge-gui-desktop/src/main/java/forge/view/HostMatch.java @@ -50,6 +50,7 @@ private HostMatch() { * @param port the local port to bind the server to */ public static void host(final int port) { + NetConnectUtil.ensurePlayerName(); SwingUtilities.invokeLater(() -> { try { System.out.println("[HostMatch] moving to network lobby screen..."); diff --git a/forge-gui-desktop/src/main/java/forge/view/JoinMatch.java b/forge-gui-desktop/src/main/java/forge/view/JoinMatch.java index 766cb480e18b..b41003abfce3 100644 --- a/forge-gui-desktop/src/main/java/forge/view/JoinMatch.java +++ b/forge-gui-desktop/src/main/java/forge/view/JoinMatch.java @@ -21,6 +21,7 @@ import forge.gui.framework.EDocID; import forge.screens.home.CHomeUI; import forge.screens.home.online.CSubmenuOnlineLobby; +import forge.gamemodes.net.NetConnectUtil; /** * Command-line "join" mode launcher. @@ -48,6 +49,8 @@ public static void join(final String serverUrl) { return; } + NetConnectUtil.ensurePlayerName(); + // Navigate to and prepare the online lobby screen, then join the server. // Done on the EDT since it manipulates Swing state and mirrors the // normal "Join A Game" button flow.