package games.strategy.engine.framework.startup.ui; import java.awt.Frame; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Observer; import javax.swing.JComponent; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import games.strategy.debug.ClientLogger; import games.strategy.debug.HeartBeat; import games.strategy.engine.ClientContext; import games.strategy.engine.data.events.GameStepListener; import games.strategy.engine.framework.GameRunner; import games.strategy.engine.framework.IGame; import games.strategy.engine.framework.headlessGameServer.HeadlessGameServer; import games.strategy.engine.framework.startup.mc.GameSelectorModel; import games.strategy.engine.lobby.server.GameDescription; import games.strategy.engine.lobby.server.GameDescription.GameStatus; import games.strategy.engine.lobby.server.ILobbyGameController; import games.strategy.engine.lobby.server.LobbyServer; import games.strategy.engine.lobby.server.RemoteHostUtils; import games.strategy.engine.lobby.server.login.LobbyLoginValidator; import games.strategy.engine.message.IRemoteMessenger; import games.strategy.engine.message.RemoteMessenger; import games.strategy.engine.message.unifiedmessenger.UnifiedMessenger; import games.strategy.net.ClientMessenger; import games.strategy.net.GUID; import games.strategy.net.IConnectionChangeListener; import games.strategy.net.IConnectionLogin; import games.strategy.net.IMessenger; import games.strategy.net.IMessengerErrorListener; import games.strategy.net.INode; import games.strategy.net.IServerMessenger; import games.strategy.net.MacFinder; import games.strategy.net.OpenFileUtility; import games.strategy.net.UniversalPlugAndPlayHelper; /** * Watches a game in progress, and updates the Lobby with the state of the game. * * <p> * This class opens its own connection to the lobby, and its own messenger. * </p> */ public class InGameLobbyWatcher { public static final String LOBBY_WATCHER_NAME = "lobby_watcher"; // this is the messenger used by the game // it is different than the messenger we use to connect to // the game lobby private final IServerMessenger m_gameMessenger; private boolean m_shutdown = false; private final GUID m_gameID = new GUID(); private GameSelectorModel m_gameSelectorModel; private final Observer m_gameSelectorModelObserver = (o, arg) -> gameSelectorModelUpdated(); private IGame m_game; private final GameStepListener m_gameStepListener = (stepName, delegateName, player, round, displayName) -> InGameLobbyWatcher.this.gameStepChanged(round); // we create this messenger, and use it to connect to the // game lobby private final IMessenger m_messenger; private final IRemoteMessenger m_remoteMessenger; private final GameDescription m_gameDescription; private final Object m_mutex = new Object(); private final IConnectionChangeListener m_connectionChangeListener; private final IMessengerErrorListener m_messengerErrorListener; /** * Reads SystemProperties to see if we should connect to a lobby server * * <p> * After creation, those properties are cleared, since we should watch the first start game. * </p> * * @return null if no watcher should be created */ public static InGameLobbyWatcher newInGameLobbyWatcher(final IServerMessenger gameMessenger, final JComponent parent, final InGameLobbyWatcher oldWatcher) { final String host = System.getProperties().getProperty(GameRunner.LOBBY_HOST); final String port = System.getProperties().getProperty(LobbyServer.TRIPLEA_LOBBY_PORT_PROPERTY); final String hostedBy = System.getProperties().getProperty(GameRunner.LOBBY_GAME_HOSTED_BY); if (host == null || port == null) { return null; } // clear the properties System.getProperties().remove(GameRunner.LOBBY_HOST); System.getProperties().remove(LobbyServer.TRIPLEA_LOBBY_PORT_PROPERTY); System.getProperties().remove(GameRunner.LOBBY_GAME_HOSTED_BY); // add them as temporary properties (in case we load an old savegame and need them again) System.getProperties().setProperty(GameRunner.LOBBY_HOST + GameRunner.OLD_EXTENSION, host); System.getProperties().setProperty(LobbyServer.TRIPLEA_LOBBY_PORT_PROPERTY + GameRunner.OLD_EXTENSION, port); System.getProperties().setProperty(GameRunner.LOBBY_GAME_HOSTED_BY + GameRunner.OLD_EXTENSION, hostedBy); final IConnectionLogin login = new IConnectionLogin() { @Override public void notifyFailedLogin(final String message) {} @Override public Map<String, String> getProperties(final Map<String, String> challengProperties) { final Map<String, String> rVal = new HashMap<>(); rVal.put(LobbyLoginValidator.ANONYMOUS_LOGIN, Boolean.TRUE.toString()); rVal.put(LobbyLoginValidator.LOBBY_VERSION, LobbyServer.LOBBY_VERSION.toString()); rVal.put(LobbyLoginValidator.LOBBY_WATCHER_LOGIN, Boolean.TRUE.toString()); return rVal; } }; try { System.out.println("host:" + host + " port:" + port); final String mac = MacFinder.getHashedMacAddress(); final ClientMessenger messenger = new ClientMessenger(host, Integer.parseInt(port), getRealName(hostedBy) + "_" + LOBBY_WATCHER_NAME, mac, login); final UnifiedMessenger um = new UnifiedMessenger(messenger); final RemoteMessenger rm = new RemoteMessenger(um); final HeartBeat h = new HeartBeat(messenger.getServerNode()); rm.registerRemote(h, HeartBeat.getHeartBeatName(um.getLocalNode())); final RemoteHostUtils rhu = new RemoteHostUtils(messenger.getServerNode(), gameMessenger); rm.registerRemote(rhu, RemoteHostUtils.getRemoteHostUtilsName(um.getLocalNode())); return new InGameLobbyWatcher(messenger, rm, gameMessenger, parent, oldWatcher); } catch (final Exception e) { ClientLogger.logQuietly(e); return null; } } private static String getRealName(final String uniqueName) { // Remove any (n) that is added to distinguish duplicate names final String name = uniqueName.split(" ")[0]; return name; } public void setGame(final IGame game) { if (m_game != null) { m_game.removeGameStepListener(m_gameStepListener); } m_game = game; if (game != null) { game.addGameStepListener(m_gameStepListener); gameStepChanged(game.getData().getSequence().getRound()); } } private void gameStepChanged(final int round) { synchronized (m_mutex) { if (!m_gameDescription.getRound().equals(Integer.toString(round))) { m_gameDescription.setRound(round + ""); } postUpdate(); } } private void gameSelectorModelUpdated() { synchronized (m_mutex) { m_gameDescription.setGameName(m_gameSelectorModel.getGameName()); m_gameDescription.setGameVersion(m_gameSelectorModel.getGameVersion()); postUpdate(); } } public InGameLobbyWatcher(final IMessenger messenger, final IRemoteMessenger remoteMessenger, final IServerMessenger serverMessenger, final JComponent parent, final InGameLobbyWatcher oldWatcher) { m_messenger = messenger; m_remoteMessenger = remoteMessenger; m_gameMessenger = serverMessenger; final String password = System.getProperty(GameRunner.TRIPLEA_SERVER_PASSWORD_PROPERTY); final boolean passworded = password != null && password.length() > 0; final Date startDateTime = (oldWatcher == null || oldWatcher.m_gameDescription == null || oldWatcher.m_gameDescription.getStartDateTime() == null) ? new Date() : oldWatcher.m_gameDescription.getStartDateTime(); final int playerCount = (oldWatcher == null || oldWatcher.m_gameDescription == null) ? (HeadlessGameServer.headless() ? 0 : 1) : oldWatcher.m_gameDescription.getPlayerCount(); final GameStatus gameStatus = (oldWatcher == null || oldWatcher.m_gameDescription == null || oldWatcher.m_gameDescription.getStatus() == null) ? GameStatus.WAITING_FOR_PLAYERS : oldWatcher.m_gameDescription.getStatus(); final String gameRound = (oldWatcher == null || oldWatcher.m_gameDescription == null || oldWatcher.m_gameDescription.getRound() == null) ? "-" : oldWatcher.m_gameDescription.getRound(); m_gameDescription = new GameDescription(m_messenger.getLocalNode(), m_gameMessenger.getLocalNode().getPort(), startDateTime, "???", playerCount, gameStatus, gameRound, m_gameMessenger.getLocalNode().getName(), System.getProperty(GameRunner.LOBBY_GAME_COMMENTS), passworded, ClientContext.engineVersion().toString(), "0"); final ILobbyGameController controller = (ILobbyGameController) m_remoteMessenger.getRemote(ILobbyGameController.GAME_CONTROLLER_REMOTE); synchronized (m_mutex) { controller.postGame(m_gameID, (GameDescription) m_gameDescription.clone()); } m_messengerErrorListener = (messenger1, reason) -> shutDown(); m_messenger.addErrorListener(m_messengerErrorListener); m_connectionChangeListener = new IConnectionChangeListener() { @Override public void connectionRemoved(final INode to) { updatePlayerCount(); } @Override public void connectionAdded(final INode to) { updatePlayerCount(); } }; // when players join or leave the game // update the connection count m_gameMessenger.addConnectionChangeListener(m_connectionChangeListener); if (oldWatcher != null && oldWatcher.m_gameDescription != null) { this.setGameStatus(oldWatcher.m_gameDescription.getStatus(), oldWatcher.m_game); } // if we loose our connection, then shutdown final Runnable r = () -> { final String addressUsed = controller.testGame(m_gameID); // if the server cannot connect to us, then quit if (addressUsed != null) { if (isActive()) { shutDown(); SwingUtilities.invokeLater(() -> { String portString = System.getProperty(GameRunner.TRIPLEA_PORT_PROPERTY); if (portString == null || portString.trim().length() <= 0) { portString = "3300"; } final String message = "Your computer is not reachable from the internet.\r\n" + "Please make sure your Firewall allows incoming connections (hosting) for TripleA.\r\n" + "(The firewall exception must be updated every time a new version of TripleA comes out.)\r\n" + "And that your Router is configured to send TCP traffic on port " + portString + " to your local ip address.\r\n" + "See 'How To Host...' in the help menu, at the top of the lobby screen.\r\n" + "The server tried to connect to your external ip: " + addressUsed + "\r\n"; if (HeadlessGameServer.headless()) { System.out.println(message); System.exit(-1); } final int port = Integer.parseInt(portString); final Frame parentComponent = JOptionPane.getFrameForComponent(parent); JOptionPane.showMessageDialog(parentComponent, message, "Could Not Host", JOptionPane.ERROR_MESSAGE); final String question = "TripleA has a new feature (in BETA) that will attempt to set your Port Forwarding for you.\r\n" + "You must have Universal Plug and Play (UPnP) enabled on your router.\r\n" + "Only around half of all routers come with UPnP enabled by default.\r\n\r\n" + "If this does not work, try turning on UPnP in your router, then try this all again.\r\n" + "(To change your router's settings, click 'How To Host...' in the help menu, or use google " + "search.)\r\n\r\n" + "If TripleA previously successfully set your port forwarding, but you still cannot host, \r\n" + "then the problem is most likely your firewall. Try creating an exception for TripleA in the " + "firewall.\r\n" + "Or disable the firewall briefly just to test.\r\n" + "The firewall exception must be updated every time a new version of TripleA comes out.\r\n"; final int answer = JOptionPane.showConfirmDialog(parentComponent, question, "Try Setting Port Forwarding with UPnP?", JOptionPane.YES_NO_OPTION); if (answer != JOptionPane.YES_OPTION) { System.exit(-1); } UniversalPlugAndPlayHelper.attemptAddingPortForwarding(parentComponent, port); if (JOptionPane.showConfirmDialog(parentComponent, "Do you want to view the tutorial on how to host? This will open in your internet browser.", "View Help Website?", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) { OpenFileUtility.openURL( "http://tripleadev.1671093.n2.nabble.com/Download-Maps-Links-Hosting-Games-General-Information-tp4074312p4085700.html"); } System.exit(-1); }); } } }; new Thread(r).start(); } public void setGameSelectorModel(final GameSelectorModel model) { cleanUpGameModelListener(); if (model != null) { m_gameSelectorModel = model; m_gameSelectorModel.addObserver(m_gameSelectorModelObserver); gameSelectorModelUpdated(); } } private void cleanUpGameModelListener() { if (m_gameSelectorModel != null) { m_gameSelectorModel.deleteObserver(m_gameSelectorModelObserver); } } protected void updatePlayerCount() { synchronized (m_mutex) { m_gameDescription.setPlayerCount(m_gameMessenger.getNodes().size() - (HeadlessGameServer.headless() ? 1 : 0)); postUpdate(); } } private void postUpdate() { if (m_shutdown) { return; } synchronized (m_mutex) { final ILobbyGameController controller = (ILobbyGameController) m_remoteMessenger.getRemote(ILobbyGameController.GAME_CONTROLLER_REMOTE); controller.updateGame(m_gameID, (GameDescription) m_gameDescription.clone()); } } public void shutDown() { m_shutdown = true; m_messenger.removeErrorListener(m_messengerErrorListener); m_messenger.shutDown(); m_gameMessenger.removeConnectionChangeListener(m_connectionChangeListener); cleanUpGameModelListener(); if (m_game != null) { m_game.removeGameStepListener(m_gameStepListener); } } public boolean isActive() { return !m_shutdown; } public void setGameStatus(final GameDescription.GameStatus status, final IGame game) { synchronized (m_mutex) { m_gameDescription.setStatus(status); if (game == null) { m_gameDescription.setRound("-"); } else { m_gameDescription.setRound(game.getData().getSequence().getRound() + ""); } setGame(game); postUpdate(); } } public String getComments() { return m_gameDescription.getComment(); } public GameDescription getGameDescription() { return m_gameDescription; } public void setGameComments(final String comments) { synchronized (m_mutex) { m_gameDescription.setComment(comments); postUpdate(); } } public void setPassworded(final boolean passworded) { synchronized (m_mutex) { m_gameDescription.setPassworded(passworded); postUpdate(); } } }