Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
package forge.screens.home.online;

import java.awt.Font;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.net.BindException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import javax.swing.JMenu;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

import com.google.common.collect.ImmutableList;
import net.miginfocom.swing.MigLayout;

import forge.gamemodes.net.ChatMessage;
import forge.gamemodes.net.NetConnectUtil;
import forge.gamemodes.net.server.FServerManager;
import forge.gui.FNetOverlay;
import forge.gui.FThreads;
import forge.gui.SOverlayUtils;
Expand All @@ -18,13 +28,18 @@
import forge.gui.framework.ICDoc;
import forge.gui.util.SOptionPane;
import forge.localinstance.properties.ForgeConstants;
import forge.localinstance.properties.ForgeNetPreferences;
import forge.menus.IMenuProvider;
import forge.menus.MenuUtil;
import forge.model.FModel;
import forge.screens.home.CHomeUI;
import forge.screens.home.CLobby;
import forge.screens.home.VLobby;
import forge.screens.home.sanctioned.ConstructedGameMenu;
import forge.toolbox.FButton;
import forge.toolbox.FLabel;
import forge.toolbox.FOptionPane;
import forge.toolbox.FSkin;
import forge.util.Localizer;

public enum CSubmenuOnlineLobby implements ICDoc, IMenuProvider {
Expand Down Expand Up @@ -75,10 +90,60 @@ private void host() {
if (CHomeUI.SINGLETON_INSTANCE.getCurrentDocID() == EDocID.HOME_NETWORK) {
VSubmenuOnlineLobby.SINGLETON_INSTANCE.populate();
}
NetConnectUtil.copyHostedServerUrl();
showServerAddressesDialog();
});
}

private void showServerAddressesDialog() {
final int port = FModel.getNetPreferences().getPrefInt(ForgeNetPreferences.FNetPref.NET_PORT);
final LinkedHashMap<String, String> addresses = FServerManager.getAllLocalAddresses();
final String externalAddress = FServerManager.getExternalAddress();
final Localizer localizer = Localizer.getInstance();

final JPanel panel = new JPanel(new MigLayout("insets 0, gap 4 6, wrap 3", "[grow][grow][pref]"));
panel.setOpaque(false);

panel.add(new FLabel.Builder().text(localizer.getMessage("lblInterface")).fontStyle(Font.BOLD).fontSize(12).build(), "growx");
panel.add(new FLabel.Builder().text(localizer.getMessage("lblAddress")).fontStyle(Font.BOLD).fontSize(12).build(), "growx");
panel.add(new FLabel.Builder().text("").build());

if (externalAddress != null) {
final String externalUrl = externalAddress + ":" + port;
panel.add(new FLabel.Builder().text("External (WAN)").fontSize(12).build(), "growx");
panel.add(new FLabel.Builder().text(externalUrl).fontSize(12).build(), "growx");
final FButton btnCopy = new FButton(localizer.getMessage("lblCopy"));
btnCopy.setFont(FSkin.getFont(11));
btnCopy.addActionListener(e -> copyToClipboard(externalUrl));
panel.add(btnCopy, "w 70!, h 24!");
}

boolean first = true;
for (final Map.Entry<String, String> entry : addresses.entrySet()) {
final String url = entry.getValue() + ":" + port;
final String label = first ? entry.getKey() + " \u2605" : entry.getKey();
first = false;

panel.add(new FLabel.Builder().text(label).fontSize(12).build(), "growx");
panel.add(new FLabel.Builder().text(url).fontSize(12).build(), "growx");
final FButton btnCopy = new FButton(localizer.getMessage("lblCopy"));
btnCopy.setFont(FSkin.getFont(11));
btnCopy.addActionListener(e -> copyToClipboard(url));
panel.add(btnCopy, "w 70!, h 24!");
}

FOptionPane.showOptionDialog(
localizer.getMessage("lblChooseAddressToCopy"),
localizer.getMessage("lblServerURL"),
FOptionPane.INFORMATION_ICON,
panel,
ImmutableList.of(localizer.getMessage("lblOK")),
0);
}

private static void copyToClipboard(final String text) {
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(text), null);
}

private void join(final String url) {
SwingUtilities.invokeLater(() -> {
SOverlayUtils.startGameOverlay(Localizer.getInstance().getMessage("lblConnectingToServer"));
Expand Down
3 changes: 3 additions & 0 deletions forge-gui/res/languages/en-US.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3127,6 +3127,9 @@ lblHostingPortOnN=Hosting on port {0}.
lblShareURLToMakePlayerJoinServer=Share a URL below with anyone who wishes to join your server.\nThe external URL has been copied to your clipboard.\n\nExternal URL (for players outside your network):\n{0}\n\nLocal URL (for players on your network):\n{1}
lblForgeUnableDetermineYourExternalIP=Forge was unable to determine your external IP.\nThe local URL has been copied to your clipboard.\n\nLocal URL (for players on your network):\n{0}
lblServerURL=Server URL
lblInterface=Interface
lblAddress=Address
lblChooseAddressToCopy=Click "Copy" next to the address you want to share with other players.
lblCopyExternalURL=Copy External URL
lblCopyLocalURL=Copy Local URL
lblYourConnectionToHostWasInterrupted=Your connection to the host ({0}) was interrupted.
Expand Down
104 changes: 104 additions & 0 deletions forge-gui/src/main/java/forge/gamemodes/net/server/FServerManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,110 @@ public static String getLocalAddress() {
}
}

/**
* Returns all usable IPv4 addresses from all network interfaces.
* Each entry maps a friendly display name to its IPv4 address.
* Results are ordered: routable address first, then others alphabetically.
*/
public static LinkedHashMap<String, String> getAllLocalAddresses() {
final LinkedHashMap<String, String> result = new LinkedHashMap<>();
final String routableAddress = getLocalAddress();

try {
final Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
final TreeMap<String, String> sorted = new TreeMap<>();

while (interfaces.hasMoreElements()) {
final NetworkInterface iface = interfaces.nextElement();
if (!iface.isUp() || iface.isLoopback()) {
continue;
}
final Enumeration<InetAddress> addresses = iface.getInetAddresses();
while (addresses.hasMoreElements()) {
final InetAddress addr = addresses.nextElement();
if (addr instanceof Inet4Address && !addr.isLoopbackAddress()) {
final String ip = addr.getHostAddress();
final String name = getFriendlyInterfaceName(iface.getName(), iface.getDisplayName(), ip);
if (ip.equals(routableAddress)) {
result.put(name, ip);
} else {
sorted.put(name, ip);
}
}
}
}
result.putAll(sorted);
} catch (final SocketException e) {
netLog.error(e, "Failed to enumerate network interfaces");
if (result.isEmpty()) {
result.put("Default", routableAddress);
}
}

if (result.isEmpty()) {
result.put("Default", routableAddress);
}
return result;
}

private static String getFriendlyInterfaceName(final String ifName, final String displayName, final String ip) {
final String lower = ifName.toLowerCase();
final String lowerDisplay = displayName.toLowerCase();

if (lower.startsWith("ham") || lowerDisplay.contains("hamachi")) {
return "Hamachi";
}
if (lower.startsWith("zt") || lowerDisplay.contains("zerotier")) {
return "ZeroTier";
}
if (isTailscaleAddress(ip)) {
return "Tailscale";
}
if (lower.startsWith("wg")) {
return "WireGuard";
}
if ((lower.startsWith("tun") && !lower.startsWith("utun")) || lower.startsWith("tap")) {
return "VPN (" + ifName + ")";
}
if (lower.startsWith("utun")) {
return "VPN Tunnel";
}
if (lower.startsWith("feth")) {
return "Virtual Network";
}
if (lower.startsWith("en")) {
if (lowerDisplay.contains("wi-fi") || lowerDisplay.contains("wifi") || lowerDisplay.contains("airport")) {
return "Wi-Fi";
}
if (lowerDisplay.contains("thunderbolt") || lowerDisplay.contains("ethernet")) {
return "Ethernet";
}
return "LAN (" + ifName + ")";
}
if (lower.startsWith("eth") || lower.startsWith("ens") || lower.startsWith("enp")) {
return "Ethernet";
}
if (lower.startsWith("wl")) {
return "Wi-Fi";
}
if (lowerDisplay.contains("radmin")) {
return "Radmin VPN";
}
return displayName;
}

private static boolean isTailscaleAddress(final String ip) {
try {
final String[] parts = ip.split("\\.");
if (parts.length == 4) {
final int first = Integer.parseInt(parts[0]);
final int second = Integer.parseInt(parts[1]);
return first == 100 && second >= 64 && second <= 127;
}
} catch (final NumberFormatException ignored) { }
return false;
}

public static String getExternalAddress() {
BufferedReader in = null;
try {
Expand Down
Loading