Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions docs/Network-Play.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (=FORGE on older phone keypads).
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.

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package forge.screens.home.online;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;

import javax.swing.BorderFactory;
import javax.swing.JPanel;
import javax.swing.SwingConstants;

import forge.deckchooser.FDeckChooser;
import forge.gamemodes.match.GameLobby;
Expand All @@ -21,6 +27,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;
Expand Down Expand Up @@ -64,11 +71,69 @@ 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 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("<html><u>" + localizer.getMessage("lblNetworkPlayGuide") + "</u></html>")
.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"));
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(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());
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();
Expand Down
10 changes: 10 additions & 0 deletions forge-gui-mobile/src/forge/screens/constructed/LobbyScreen.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
6 changes: 3 additions & 3 deletions forge-gui-mobile/src/forge/screens/home/HomeScreen.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
Loading