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() { diff --git a/forge-game/src/main/java/forge/trackable/TrackableObject.java b/forge-game/src/main/java/forge/trackable/TrackableObject.java index 895a715294e..78017583334 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; } @@ -45,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); 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); 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/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); 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 5ebf3d969b2..c80beca4b1d 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; @@ -75,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 @@ -124,9 +129,13 @@ 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); } + @Override + public ClientGameLobby getLobby() { + return lobby; + } }); view.setPlayerChangeListener(new IPlayerChangeListener() { @Override @@ -149,7 +158,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)); } 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); } 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}); + } + } } } 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();