Skip to content
14 changes: 14 additions & 0 deletions docs/Network-Play.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down
81 changes: 81 additions & 0 deletions forge-gui-desktop/src/main/java/forge/view/HostMatch.java
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/
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.
* <p>
* 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}.
* <p>
* 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();
}
});
}
}
62 changes: 62 additions & 0 deletions forge-gui-desktop/src/main/java/forge/view/JoinMatch.java
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/
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.
* <p>
* 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"}).
* <p>
* 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);
});
}
}
25 changes: 24 additions & 1 deletion forge-gui-desktop/src/main/java/forge/view/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
22 changes: 21 additions & 1 deletion forge-gui/src/main/java/forge/gamemodes/net/NetConnectUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
* <p>
* 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 {
Expand Down