diff --git a/docs/Network-Play.md b/docs/Network-Play.md index 0a0d593c8ff7..3561bdc422c0 100644 --- a/docs/Network-Play.md +++ b/docs/Network-Play.md @@ -64,6 +64,20 @@ --- +# 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 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 +> - 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] 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..be7d7e56cac7 --- /dev/null +++ b/forge-gui-desktop/src/main/java/forge/view/HostMatch.java @@ -0,0 +1,81 @@ +/* + * 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) { + NetConnectUtil.ensurePlayerName(); + 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..b41003abfce3 --- /dev/null +++ b/forge-gui-desktop/src/main/java/forge/view/JoinMatch.java @@ -0,0 +1,62 @@ +/* + * 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; +import forge.gamemodes.net.NetConnectUtil; + +/** + * 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; + } + + 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. + 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..bfd33e287229 100644 --- a/forge-gui-desktop/src/main/java/forge/view/Main.java +++ b/forge-gui-desktop/src/main/java/forge/view/Main.java @@ -85,8 +85,31 @@ 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' "); + System.out.println("Unknown mode.\nKnown mode is 'sim', 'parse', 'host', 'join' "); 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 {