From daf3be6346f70e72d2b195bbd20b93af1c5ee589 Mon Sep 17 00:00:00 2001 From: "Jamin W. Collins" Date: Tue, 13 Feb 2018 18:16:10 -0700 Subject: [PATCH 01/10] Improve reported multiplayer host IP The existing IP selection logic was less than optimal on Linux hosts where it would frequently select and report 127.0.0.1 as the IP address to share. The new logic will create a datagram socket, use it to locate an interface with a default route, and further backtrack to the interface's IPv4 address. Signed-off-by: Jamin W. Collins --- .../java/forge/net/server/FServerManager.java | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/forge-gui/src/main/java/forge/net/server/FServerManager.java b/forge-gui/src/main/java/forge/net/server/FServerManager.java index eae6497446c..8b8416c8a04 100644 --- a/forge-gui/src/main/java/forge/net/server/FServerManager.java +++ b/forge-gui/src/main/java/forge/net/server/FServerManager.java @@ -30,9 +30,16 @@ import io.netty.handler.codec.serialization.ObjectEncoder; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; +import java.net.DatagramSocket; +import java.net.Inet4Address; +import java.net.Inet6Address; import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.UnknownHostException; import java.util.Collection; import java.util.Collections; +import java.util.Enumeration; import java.util.Map; import org.apache.log4j.PropertyConfigurator; @@ -198,9 +205,35 @@ public final class FServerManager { return null; } + // inspired by: + // https://stackoverflow.com/a/34873630 + // https://stackoverflow.com/a/901943 + private String getRoutableAddress(boolean preferIpv4, boolean preferIPv6) throws SocketException, UnknownHostException { + DatagramSocket s = new DatagramSocket(); + s.connect(InetAddress.getByAddress(new byte[]{8,8,8,8}), 0); + NetworkInterface n = NetworkInterface.getByInetAddress(s.getLocalAddress()); + Enumeration en = n.getInetAddresses(); + while (en.hasMoreElements()) { + InetAddress addr = (InetAddress) en.nextElement(); + if (addr instanceof Inet4Address) { + if (preferIPv6) { + continue; + } + return addr.getHostAddress(); + } + if (addr instanceof Inet6Address) { + if (preferIpv4) { + continue; + } + return addr.getHostAddress(); + } + } + return null; + } + public String getLocalAddress() { try { - return InetAddress.getLocalHost().getHostAddress(); + return getRoutableAddress(true, false); } catch (final Exception e) { e.printStackTrace(); From 711342829b1392dca690bb0ffb79245c34692231 Mon Sep 17 00:00:00 2001 From: "Jamin W. Collins" Date: Tue, 13 Feb 2018 18:28:47 -0700 Subject: [PATCH 02/10] Improve interruption messaging Signed-off-by: Jamin W. Collins --- forge-gui/src/main/java/forge/net/NetConnectUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge-gui/src/main/java/forge/net/NetConnectUtil.java b/forge-gui/src/main/java/forge/net/NetConnectUtil.java index 5ebf3d969b2..635cd0d17a8 100644 --- a/forge-gui/src/main/java/forge/net/NetConnectUtil.java +++ b/forge-gui/src/main/java/forge/net/NetConnectUtil.java @@ -124,7 +124,7 @@ public class NetConnectUtil { } @Override public final void close() { - SOptionPane.showMessageDialog("Connection to the host was interrupted.", "Error", FSkinProp.ICO_WARNING); + SOptionPane.showMessageDialog("Your connection to the host (" + url + ") was interrupted.", "Error", FSkinProp.ICO_WARNING); onlineLobby.setClient(null); } }); From fbf0f98c3a8102d747b5138e2f79caa5a7f4c095 Mon Sep 17 00:00:00 2001 From: "Jamin W. Collins" Date: Tue, 13 Feb 2018 18:30:58 -0700 Subject: [PATCH 03/10] Limit team editing to each player Signed-off-by: Jamin W. Collins --- .../src/main/java/forge/screens/home/PlayerPanel.java | 1 + 1 file changed, 1 insertion(+) diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/PlayerPanel.java b/forge-gui-desktop/src/main/java/forge/screens/home/PlayerPanel.java index 8eed2d60838..50a4ae31fcd 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/PlayerPanel.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/PlayerPanel.java @@ -194,6 +194,7 @@ public class PlayerPanel extends FPanel { txtPlayerName.setEnabled(mayEdit); txtPlayerName.setText(type == LobbySlotType.OPEN ? StringUtils.EMPTY : playerName); nameRandomiser.setEnabled(mayEdit); + teamComboBox.setEnabled(mayEdit); deckLabel.setVisible(mayEdit); deckBtn.setVisible(mayEdit); chkReady.setVisible(type == LobbySlotType.LOCAL || type == LobbySlotType.REMOTE); From 375adab087caa955d0b4f4f7491a3ef6262dfc96 Mon Sep 17 00:00:00 2001 From: "Jamin W. Collins" Date: Tue, 13 Feb 2018 18:33:25 -0700 Subject: [PATCH 04/10] gracefully handle failure to connect to server Signed-off-by: Jamin W. Collins --- .../forge/screens/home/online/CSubmenuOnlineLobby.java | 8 +++++--- forge-gui/src/main/java/forge/net/NetConnectUtil.java | 8 +++++++- 2 files changed, 12 insertions(+), 4 deletions(-) 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 c4069d1fbe3..2a266eca88a 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 @@ -86,9 +86,11 @@ public enum CSubmenuOnlineLobby implements ICDoc, IMenuProvider { @Override public void run() { SOverlayUtils.hideOverlay(); - FNetOverlay.SINGLETON_INSTANCE.show(result); - if (CHomeUI.SINGLETON_INSTANCE.getCurrentDocID() == EDocID.HOME_NETWORK) { - VSubmenuOnlineLobby.SINGLETON_INSTANCE.populate(); + if (result instanceof ChatMessage) { + FNetOverlay.SINGLETON_INSTANCE.show(result); + if (CHomeUI.SINGLETON_INSTANCE.getCurrentDocID() == EDocID.HOME_NETWORK) { + VSubmenuOnlineLobby.SINGLETON_INSTANCE.populate(); + } } } }); diff --git a/forge-gui/src/main/java/forge/net/NetConnectUtil.java b/forge-gui/src/main/java/forge/net/NetConnectUtil.java index 635cd0d17a8..96c3632cecf 100644 --- a/forge-gui/src/main/java/forge/net/NetConnectUtil.java +++ b/forge-gui/src/main/java/forge/net/NetConnectUtil.java @@ -1,5 +1,6 @@ package forge.net; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import forge.GuiBase; @@ -149,7 +150,12 @@ public class NetConnectUtil { catch (Exception ex) {} } - client.connect(hostname, port); + try { + client.connect(hostname, port); + } + catch (Exception ex) { + return null; + } return new ChatMessage(null, String.format("Connected to %s:%d", hostname, port)); } From 3a6c5b0cd5caad90503ff63fdc50ca260150d020 Mon Sep 17 00:00:00 2001 From: "Jamin W. Collins" Date: Tue, 13 Feb 2018 18:36:19 -0700 Subject: [PATCH 05/10] Allow arbitrarily setting a Tracker Signed-off-by: Jamin W. Collins --- .../src/main/java/forge/trackable/TrackableObject.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/forge-game/src/main/java/forge/trackable/TrackableObject.java b/forge-game/src/main/java/forge/trackable/TrackableObject.java index 895a715294e..e654d9938fd 100644 --- a/forge-game/src/main/java/forge/trackable/TrackableObject.java +++ b/forge-game/src/main/java/forge/trackable/TrackableObject.java @@ -14,7 +14,7 @@ public abstract class TrackableObject implements IIdentifiable, Serializable { private static final long serialVersionUID = 7386836745378571056L; private final int id; - protected final transient Tracker tracker; + protected transient Tracker tracker; private final Map props; private final Set changedProps; private boolean copyingProps; @@ -30,6 +30,11 @@ public abstract class TrackableObject implements IIdentifiable, Serializable { return id; } + // needed for multiplayer support + public void setTracker(Tracker tracker) { + this.tracker = tracker; + } + public final Tracker getTracker() { return tracker; } From 367fcad53ccd06bd25bfead19dd36176904b41e2 Mon Sep 17 00:00:00 2001 From: "Jamin W. Collins" Date: Tue, 13 Feb 2018 18:38:58 -0700 Subject: [PATCH 06/10] Define getter for GameView Signed-off-by: Jamin W. Collins --- forge-gui/src/main/java/forge/interfaces/IGuiGame.java | 1 + 1 file changed, 1 insertion(+) diff --git a/forge-gui/src/main/java/forge/interfaces/IGuiGame.java b/forge-gui/src/main/java/forge/interfaces/IGuiGame.java index 37a72962b86..5b3ab7c1568 100644 --- a/forge-gui/src/main/java/forge/interfaces/IGuiGame.java +++ b/forge-gui/src/main/java/forge/interfaces/IGuiGame.java @@ -25,6 +25,7 @@ import forge.util.ITriggerEvent; public interface IGuiGame { void setGameView(GameView gameView); + GameView getGameView(); void setOriginalGameController(PlayerView view, IGameController gameController); void setGameController(PlayerView player, IGameController gameController); void setSpectator(IGameController spectator); From e8c9c8b90cc6c0674e038b0f6a1a68b9c808c6e8 Mon Sep 17 00:00:00 2001 From: "Jamin W. Collins" Date: Tue, 13 Feb 2018 18:41:56 -0700 Subject: [PATCH 07/10] Define getter for ClientGameLobby Signed-off-by: Jamin W. Collins --- .../src/main/java/forge/interfaces/ILobbyListener.java | 2 ++ forge-gui/src/main/java/forge/net/NetConnectUtil.java | 8 ++++++++ forge-gui/src/main/java/forge/net/client/FGameClient.java | 4 ++++ 3 files changed, 14 insertions(+) diff --git a/forge-gui/src/main/java/forge/interfaces/ILobbyListener.java b/forge-gui/src/main/java/forge/interfaces/ILobbyListener.java index 74aab55bf9a..dc1f67f7218 100644 --- a/forge-gui/src/main/java/forge/interfaces/ILobbyListener.java +++ b/forge-gui/src/main/java/forge/interfaces/ILobbyListener.java @@ -1,9 +1,11 @@ package forge.interfaces; import forge.match.GameLobby.GameLobbyData; +import forge.net.client.ClientGameLobby; public interface ILobbyListener { void message(String source, String message); void update(GameLobbyData state, int slot); void close(); + ClientGameLobby getLobby(); } diff --git a/forge-gui/src/main/java/forge/net/NetConnectUtil.java b/forge-gui/src/main/java/forge/net/NetConnectUtil.java index 96c3632cecf..c80beca4b1d 100644 --- a/forge-gui/src/main/java/forge/net/NetConnectUtil.java +++ b/forge-gui/src/main/java/forge/net/NetConnectUtil.java @@ -76,6 +76,10 @@ public class NetConnectUtil { public final void close() { // NO-OP, server can't receive close message } + @Override + public ClientGameLobby getLobby() { + return null; + } }); chatInterface.setGameClient(new IRemote() { @Override @@ -128,6 +132,10 @@ public class NetConnectUtil { SOptionPane.showMessageDialog("Your connection to the host (" + url + ") was interrupted.", "Error", FSkinProp.ICO_WARNING); onlineLobby.setClient(null); } + @Override + public ClientGameLobby getLobby() { + return lobby; + } }); view.setPlayerChangeListener(new IPlayerChangeListener() { @Override diff --git a/forge-gui/src/main/java/forge/net/client/FGameClient.java b/forge-gui/src/main/java/forge/net/client/FGameClient.java index 35cf2bc6c54..7a0f563730d 100644 --- a/forge-gui/src/main/java/forge/net/client/FGameClient.java +++ b/forge-gui/src/main/java/forge/net/client/FGameClient.java @@ -105,6 +105,10 @@ public class FGameClient implements IToServer { return replies.get(event.getId()); } + List getLobbyListeners() { + return lobbyListeners; + } + public void addLobbyListener(final ILobbyListener listener) { lobbyListeners.add(listener); } From 45a0c54fb8b2247578dd6762835103647571983a Mon Sep 17 00:00:00 2001 From: "Jamin W. Collins" Date: Tue, 13 Feb 2018 18:44:28 -0700 Subject: [PATCH 08/10] Define getter for TrackableObject props Signed-off-by: Jamin W. Collins --- .../src/main/java/forge/trackable/TrackableObject.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/forge-game/src/main/java/forge/trackable/TrackableObject.java b/forge-game/src/main/java/forge/trackable/TrackableObject.java index e654d9938fd..78017583334 100644 --- a/forge-game/src/main/java/forge/trackable/TrackableObject.java +++ b/forge-game/src/main/java/forge/trackable/TrackableObject.java @@ -50,6 +50,11 @@ public abstract class TrackableObject implements IIdentifiable, Serializable { return o.hashCode() == id && o.getClass().equals(getClass()); } + // don't know if this is really needed, but don't know a better way + public T getProps() { + return (T)props; + } + @SuppressWarnings("unchecked") protected final T get(final TrackableProperty key) { T value = (T)props.get(key); From b47d528bf06320bd403973479aac03701513406c Mon Sep 17 00:00:00 2001 From: "Jamin W. Collins" Date: Thu, 15 Feb 2018 17:07:14 -0700 Subject: [PATCH 09/10] ensure that remote client has trackers defined Signed-off-by: Jamin W. Collins --- .../forge/net/client/GameClientHandler.java | 182 +++++++++++++++++- 1 file changed, 176 insertions(+), 6 deletions(-) diff --git a/forge-gui/src/main/java/forge/net/client/GameClientHandler.java b/forge-gui/src/main/java/forge/net/client/GameClientHandler.java index d8125bee65a..f15a4576bc5 100644 --- a/forge-gui/src/main/java/forge/net/client/GameClientHandler.java +++ b/forge-gui/src/main/java/forge/net/client/GameClientHandler.java @@ -1,5 +1,14 @@ package forge.net.client; +import com.google.common.collect.Lists; +import forge.LobbyPlayer; +import forge.game.*; +import forge.game.player.RegisteredPlayer; +import forge.interfaces.ILobbyListener; +import forge.match.LobbySlot; +import forge.player.LobbyPlayerHuman; +import forge.trackable.TrackableObject; +import forge.trackable.Tracker; import io.netty.channel.ChannelHandlerContext; import forge.game.player.PlayerView; import forge.interfaces.IGuiGame; @@ -12,9 +21,12 @@ import forge.net.event.LoginEvent; import forge.properties.ForgePreferences.FPref; import forge.trackable.TrackableCollection; +import java.util.*; + final class GameClientHandler extends GameProtocolHandler { private final FGameClient client; private final IGuiGame gui; + private Tracker tracker; /** * Creates a client-side game handler. @@ -23,6 +35,7 @@ final class GameClientHandler extends GameProtocolHandler { super(true); this.client = client; this.gui = client.getGui(); + this.tracker = null; } @Override @@ -44,12 +57,169 @@ final class GameClientHandler extends GameProtocolHandler { @Override protected void beforeCall(final ProtocolMethod protocolMethod, final Object[] args) { switch (protocolMethod) { - case openView: - final TrackableCollection myPlayers = (TrackableCollection) args[0]; - client.setGameControllers(myPlayers); - break; - default: - break; + case openView: + if (this.tracker == null) { + int maxAttempts = 5; + for (int numAttempts = 0; numAttempts < maxAttempts; numAttempts++) { + try { + + this.tracker = createTracker(); + + for (PlayerView myPlayer : (TrackableCollection) args[0]) { + if (myPlayer.getTracker() == null) { + myPlayer.setTracker(this.tracker); + } + } + + final TrackableCollection myPlayers = (TrackableCollection) args[0]; + client.setGameControllers(myPlayers); + + } catch (Exception e) { + System.err.println("Failed: attempt number: " + numAttempts + " - " + e.toString()); + try { + Thread.sleep(100); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } + } + } + } + break; + default: + break; + } + if (!(this.tracker == null)) { + updateTrackers(args); + } + } + + /** + * This method attempts to recreate a GameRules object from existing state. + * + * @return GameRules + */ + private GameRules createGameRules(GameType gameType, GameView gameView) { + // FIXME: how do we know the rules are the same on each side??? + GameRules gameRules = new GameRules(gameType); + gameRules.setGamesPerMatch(gameView.getNumGamesInMatch()); + gameRules.setPoisonCountersToLose(gameView.getPoisonCountersToLose()); + + return gameRules; + } + + /** + * This method creates the necessary objects and state to retrieve a Tracker object. + * + * Near as I can tell, that means that we need to create a Match. + * + * Creating a Match requires that we have: + * * GameRules + * * RegisteredPlayers + * * Title + * + * @return Tracker + */ + private Tracker createTracker() { + + // retrieve what we can from the existing (but incomplete) state + final IGuiGame gui = client.getGui(); + GameView gameView = gui.getGameView(); + + final GameType gameType = gameView.getGameType(); + final GameRules gameRules = createGameRules(gameType, gameView); + final List registeredPlayers = createRegisteredPlayers(gameType); + + // pull the title from the existing (incomplete) GameView + final String title = gameView.getTitle(); + + // create a valid match object and game + Match match = new Match(gameRules, registeredPlayers, title); + Game game = match.createGame(); + + // replace the existing incomplete GameView with the newly created one + gui.setGameView(null); + gui.setGameView(game.getView()); + + return gui.getGameView().getTracker(); + } + + /** + * This method retrieves existing information about the players to + * build a list of RegisteredPlayers. + * + * + * @param gameType + * @return List + */ + private List createRegisteredPlayers(GameType gameType) { + // get all lobby players + List lobbyListeners = client.getLobbyListeners(); + ILobbyListener lobbyListener = lobbyListeners.get(0); + ClientGameLobby myLobby = lobbyListener.getLobby(); + + List players = Lists.newArrayList(); + int playerCount = myLobby.getNumberOfSlots(); + for (int i = 0; i < playerCount; i++) { + LobbySlot playerSlot = myLobby.getSlot(i); + RegisteredPlayer player = RegisteredPlayer.forVariants( + playerCount, + Collections.singleton(gameType), + playerSlot.getDeck(), + null, + false, + null, + null + ); + LobbyPlayer lobbyPlayer = new LobbyPlayerHuman( + playerSlot.getName(), + playerSlot.getAvatarIndex() + ); + player.setPlayer(lobbyPlayer); + player.setTeamNumber(playerSlot.getTeam()); + players.add(player); + } + + final List sortedPlayers = Lists.newArrayList(players); + Collections.sort(sortedPlayers, new Comparator() { + @Override + public final int compare(final RegisteredPlayer p1, final RegisteredPlayer p2) { + final int v1 = p1.getPlayer() instanceof LobbyPlayerHuman ? 0 : 1; + final int v2 = p2.getPlayer() instanceof LobbyPlayerHuman ? 0 : 1; + return Integer.compare(v1, v2); + } + }); + + return sortedPlayers; + } + + /** + * This method is used to recursively update the tracker + * references on all objects and their props. + * + * @param objs + */ + private void updateTrackers(final Object[] objs) { + for (Object obj: objs) { + if (obj instanceof TrackableObject) { + TrackableObject trackableObject = ((TrackableObject) obj); + if (trackableObject.getTracker() == null) { + trackableObject.setTracker(this.tracker); + // walk the props + EnumMap props = (EnumMap) trackableObject.getProps(); + if (!(props == null)) { + for (Object propObj : props.values()) { + updateTrackers(new Object[]{propObj}); + } + } + } + } else if (obj instanceof TrackableCollection) { + TrackableCollection collection = ((TrackableCollection) obj); + Iterator itrCollection = collection.iterator(); + while (itrCollection.hasNext()) { + Object objCollection = itrCollection.next(); + updateTrackers(new Object[]{objCollection}); + } + } } } From 84874601a94fd2103fe0c506ee49bda2dd1edba4 Mon Sep 17 00:00:00 2001 From: "Jamin W. Collins" Date: Thu, 15 Feb 2018 17:08:01 -0700 Subject: [PATCH 10/10] Continue but log NullPointerException in getMana Signed-off-by: Jamin W. Collins --- .../src/main/java/forge/game/player/PlayerView.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/forge-game/src/main/java/forge/game/player/PlayerView.java b/forge-game/src/main/java/forge/game/player/PlayerView.java index c023ae37e7f..f8dffa5482b 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerView.java +++ b/forge-game/src/main/java/forge/game/player/PlayerView.java @@ -373,7 +373,14 @@ public class PlayerView extends GameEntityView { } public int getMana(final byte color) { - Integer count = getMana().get(color); + Integer count = null; + try { + count = getMana().get(color); + } + catch (Exception e) { + e.printStackTrace(); + count = null; + } return count != null ? count.intValue() : 0; } private Map getMana() {