/** * Copyright (C) 2002-2012 The FreeCol Team * * This file is part of FreeCol. * * FreeCol is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * FreeCol is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with FreeCol. If not, see <http://www.gnu.org/licenses/>. */ package net.sf.freecol.client.control; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.SwingUtilities; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import net.sf.freecol.FreeCol; import net.sf.freecol.client.ClientOptions; import net.sf.freecol.client.FreeColClient; import net.sf.freecol.client.gui.GUI; import net.sf.freecol.client.gui.i18n.Messages; import net.sf.freecol.client.gui.panel.LoadingSavegameDialog; import net.sf.freecol.common.FreeColException; import net.sf.freecol.common.ServerInfo; import net.sf.freecol.common.io.FreeColDirectories; import net.sf.freecol.common.io.FreeColModFile; import net.sf.freecol.common.io.FreeColSavegameFile; import net.sf.freecol.common.model.Game; import net.sf.freecol.common.model.NationOptions.Advantages; import net.sf.freecol.common.model.Player; import net.sf.freecol.common.model.Specification; import net.sf.freecol.common.model.Tile; import net.sf.freecol.common.model.Unit; import net.sf.freecol.common.networking.Connection; import net.sf.freecol.common.networking.DOMMessage; import net.sf.freecol.common.networking.LoginMessage; import net.sf.freecol.common.networking.NoRouteToServerException; import net.sf.freecol.common.option.OptionGroup; import net.sf.freecol.common.resources.ResourceManager; import net.sf.freecol.common.util.XMLStream; import net.sf.freecol.server.FreeColServer; import org.w3c.dom.Element; import org.w3c.dom.NodeList; /** * The controller responsible for starting a server and connecting to it. * {@link PreGameInputHandler} will be set as the input handler when a * successful login has been completed, */ public final class ConnectController { private static final Logger logger = Logger.getLogger(ConnectController.class.getName()); private final FreeColClient freeColClient; private GUI gui; /** * Creates a new <code>ConnectController</code>. * * @param freeColClient The main client controller. */ public ConnectController(FreeColClient freeColClient, GUI gui) { this.freeColClient = freeColClient; this.gui = gui; } /** * Shut down an existing server on a given port. * * @param port The port to unblock. * @return True if there should be no blocking server remaining. */ private boolean unblockServer(int port) { FreeColServer freeColServer = freeColClient.getFreeColServer(); if (freeColServer != null && freeColServer.getServer().getPort() == port) { if (gui.showConfirmDialog("stopServer.text", "stopServer.yes", "stopServer.no")) { freeColServer.getController().shutdown(); } else { return false; } } return true; } /** * Starts a multiplayer server and connects to it. * * @param specification The <code>Specification</code> for the game. * @param publicServer Whether to make the server public. * @param userName The name to use when logging in. * @param port The port in which the server should listen for new clients. * @param advantages The national <code>Advantages</code>. * @param level An <code>OptionGroup</code> containing difficulty options. */ public void startMultiplayerGame(Specification specification, boolean publicServer, String userName, int port, Advantages advantages, OptionGroup level) { freeColClient.setMapEditor(false); if (freeColClient.isLoggedIn()) logout(true); if (!unblockServer(port)) return; FreeColServer freeColServer; try { freeColServer = new FreeColServer(specification, publicServer, false, port, null, advantages); } catch (NoRouteToServerException e) { gui.errorMessage("server.noRouteToServer"); logger.log(Level.WARNING, "No route to server.", e); return; } catch (IOException e) { gui.errorMessage("server.couldNotStart"); logger.log(Level.WARNING, "Could not start server.", e); return; } freeColClient.setFreeColServer(freeColServer); joinMultiplayerGame(userName, "localhost", port); } /** * Load current mod fragments into the specification. * * @param specification The <code>Specification</code> to load into. */ private void loadModFragments(Specification specification) { boolean loadedMod = false; for (FreeColModFile f : freeColClient.getClientOptions() .getActiveMods()) { InputStream sis = null; try { sis = f.getSpecificationInputStream(); } catch (IOException ioe) { logger.log(Level.WARNING, "IO error in mod fragment " + f.getId(), ioe); } if (sis != null) { try { specification.loadFragment(sis); loadedMod = true; logger.info("Loaded mod fragment " + f.getId()); } catch (RuntimeException rte) { logger.log(Level.WARNING, "Parse error in mod fragment " + f.getId(), rte); } } } if (loadedMod) { // Update actions in case new ones loaded. freeColClient.updateActions(); } } /** * Starts a new single player game by connecting to the server. * TODO: connect client/server directly (not using network-classes) * * @param specification The <code>Specification</code> for the game. * @param userName The name to use when logging in. * @param advantages The national <code>Advantages</code>. */ public void startSinglePlayerGame(Specification specification, String userName, Advantages advantages) { freeColClient.setMapEditor(false); if (freeColClient.isLoggedIn()) logout(true); if (!unblockServer(FreeCol.getDefaultPort())) return; loadModFragments(specification); FreeColServer freeColServer; try { freeColServer = new FreeColServer(specification, false, true, -1, null, advantages); } catch (NoRouteToServerException e) { gui.errorMessage("server.noRouteToServer"); logger.log(Level.WARNING, "No route to server (single player!).", e); return; } catch (IOException e) { gui.errorMessage("server.couldNotStart"); logger.log(Level.WARNING, "Could not start server.", e); return; } if (freeColClient.getClientOptions() .getBoolean(ClientOptions.AUTOSAVE_DELETE)) { FreeColServer.removeAutosaves(Messages.message("clientOptions.savegames.autosave.fileprefix")); } freeColClient.setFreeColServer(freeColServer); freeColClient.setSinglePlayer(true); if (login(userName, "127.0.0.1", freeColServer.getPort())) { freeColClient.getPreGameController().setReady(true); gui.showStartGamePanel(freeColClient.getGame(), freeColClient.getMyPlayer(), true); } } /** * Starts a new multiplayer game by connecting to the server. * * @param userName The name to use when logging in. * @param host The name of the machine running the server. * @param port The port to use when connecting to the host. */ public void joinMultiplayerGame(String userName, String host, int port) { freeColClient.setMapEditor(false); if (freeColClient.isLoggedIn()) logout(true); List<String> vacantPlayers = getVacantPlayers(host, port); if (vacantPlayers != null) { String choice = gui.showSimpleChoiceDialog(null, "connectController.choicePlayer", "cancel", vacantPlayers); if (choice == null) return; userName = choice; } freeColClient.setSinglePlayer(false); if (login(userName, host, port) && !freeColClient.isInGame()) { gui.showStartGamePanel(freeColClient.getGame(), freeColClient.getMyPlayer(), false); } } /** * Starts the client and connects to <i>host:port</i>. * * @param userName The name to use when logging in. This should be * a unique identifier. * @param host The name of the machine running the * <code>FreeColServer</code>. * @param port The port to use when connecting to the host. * @return True if the login succeeds. */ public boolean login(String userName, String host, int port) { freeColClient.setMapEditor(false); freeColClient.askServer().disconnect(); try { freeColClient.askServer().connect(FreeCol.CLIENT_THREAD + userName, host, port, freeColClient.getPreGameInputHandler()); } catch (Exception e) { gui.errorMessage("server.couldNotConnect", e.getMessage()); return false; } LoginMessage msg = freeColClient.askServer() .login(userName, FreeCol.getVersion()); Game game; if (msg == null || (game = msg.getGame()) == null) return false; // This completes the client's view of the spec with options // obtained from the server difficulty. It should not be // required in the client, to be removed later, when newTurn() // only runs in the server freeColClient.setGame(game); Player player = game.getPlayerByName(userName); if (player == null) { logger.warning("New game does not contain player: " + userName); return false; } freeColClient.setMyPlayer(player); freeColClient.addSpecificationActions(game.getSpecification()); logger.info("FreeColClient logged in as " + userName + "/" + player.getId()); // Reconnect if (msg.getStartGame()) { Tile entryTile = (player.getEntryLocation() == null) ? null : player.getEntryLocation().getTile(); freeColClient.setSinglePlayer(msg.isSinglePlayer()); freeColClient.getPreGameController().startGame(); if (msg.isCurrentPlayer()) { freeColClient.getInGameController() .setCurrentPlayer(player); Unit activeUnit = msg.getActiveUnit(); if (activeUnit != null) { activeUnit.getOwner().resetIterators(); activeUnit.getOwner().setNextActiveUnit(activeUnit); gui.setActiveUnit(activeUnit); } else { gui.setSelectedTile(entryTile, false); } } else { gui.setSelectedTile(entryTile, false); } } // All done. freeColClient.setLoggedIn(true); return true; } /** * Reconnects to the server. */ public void reconnect() { final String userName = freeColClient.getMyPlayer().getName(); final String host = freeColClient.getClient().getHost(); final int port = freeColClient.getClient().getPort(); gui.removeInGameComponents(); logout(true); login(userName, host, port); freeColClient.getInGameController().nextModelMessage(); } /** * Opens a dialog where the user should specify the filename * and loads the game. */ public void loadGame() { File file = gui.showLoadDialog(FreeColDirectories.getSaveDirectory()); if (file != null) { //FreeCol.setSaveDirectory(file.getParentFile()); loadGame(file); } } /** * Loads a game from the given file. * * @param file The <code>File</code>. */ public void loadGame(File file) { final File theFile = file; freeColClient.setMapEditor(false); class ErrorJob implements Runnable { private final String message; ErrorJob( String message ) { this.message = message; } public void run() { gui.closeMenus(); gui.errorMessage( message ); } } final boolean singlePlayer; final String name; final int port; XMLStream xs = null; try { // Get suggestions for "singlePlayer" and "publicServer" // settings from the file final FreeColSavegameFile fis = new FreeColSavegameFile(theFile); xs = new XMLStream(fis.getSavegameInputStream()); final XMLStreamReader in = xs.getXMLStreamReader(); in.nextTag(); String str = in.getAttributeValue(null, "singleplayer"); final boolean defaultSinglePlayer = str != null && Boolean.valueOf(str).booleanValue(); str = in.getAttributeValue(null, "publicServer"); final boolean defaultPublicServer = str != null && Boolean.valueOf(str).booleanValue(); xs.close(); // Reload the client options saved with this game. try { ClientOptions options = freeColClient.getClientOptions(); options.updateOptions(fis.getInputStream(FreeColSavegameFile.CLIENT_OPTIONS)); options.fixClientOptions(); } catch (FileNotFoundException e) { // no client options, we don't care } final int sgo = freeColClient.getClientOptions() .getInteger(ClientOptions.SHOW_SAVEGAME_SETTINGS); boolean show = sgo == ClientOptions.SHOW_SAVEGAME_SETTINGS_ALWAYS || (!defaultSinglePlayer && sgo == ClientOptions.SHOW_SAVEGAME_SETTINGS_MULTIPLAYER); if (show) { if (gui.showLoadingSavegameDialog(defaultPublicServer, defaultSinglePlayer)) { LoadingSavegameDialog lsd = gui.getLoadingSavegameDialog(); singlePlayer = lsd.isSinglePlayer(); name = lsd.getName(); port = lsd.getPort(); } else { return; } } else { singlePlayer = defaultSinglePlayer; name = null; port = -1; } } catch (FileNotFoundException e) { SwingUtilities.invokeLater(new ErrorJob("fileNotFound")); logger.log(Level.WARNING, "Can not find file: " + file.getName(), e); return; } catch (IOException e) { SwingUtilities.invokeLater(new ErrorJob("server.couldNotStart")); logger.log(Level.WARNING, "Could not start server.", e); return; } catch (XMLStreamException e) { logger.log(Level.WARNING, "Error reading game from: " + file.getName(), e); SwingUtilities.invokeLater( new ErrorJob("server.couldNotStart") ); return; } catch (Exception e) { SwingUtilities.invokeLater(new ErrorJob("couldNotLoadGame")); logger.log(Level.WARNING, "Could not load game from: " + file.getName(), e); return; } finally { if (xs != null) xs.close(); } if (!unblockServer(port)) return; gui.showStatusPanel(Messages.message("status.loadingGame")); Runnable loadGameJob = new Runnable() { public void run() { FreeColServer freeColServer = null; try { final FreeColSavegameFile saveGame = new FreeColSavegameFile(theFile); freeColServer = new FreeColServer(saveGame, port, name); freeColClient.setFreeColServer(freeColServer); final String userName = freeColServer.getOwner(); final int port = freeColServer.getPort(); freeColClient.setSinglePlayer(singlePlayer); freeColClient.getInGameController().setGameConnected(); SwingUtilities.invokeLater(new Runnable() { public void run() { ResourceManager.setScenarioMapping(saveGame.getResourceMapping()); login(userName, "127.0.0.1", port); gui.closeStatusPanel(); } }); } catch (NoRouteToServerException e) { SwingUtilities.invokeLater(new Runnable() { public void run() { gui.closeMainPanel(); gui.showMainPanel(); } }); SwingUtilities.invokeLater(new ErrorJob("server.noRouteToServer")); logger.log(Level.WARNING, "No route to server.", e); } catch (FileNotFoundException e) { SwingUtilities.invokeLater(new Runnable() { public void run() { gui.closeMainPanel(); gui.showMainPanel(); } }); SwingUtilities.invokeLater(new ErrorJob("fileNotFound")); logger.log(Level.WARNING, "Can not find file.", e); } catch (IOException e) { SwingUtilities.invokeLater(new Runnable() { public void run() { gui.closeMainPanel(); gui.showMainPanel(); } }); SwingUtilities.invokeLater(new ErrorJob("server.couldNotStart")); logger.log(Level.WARNING, "Error starting game.", e); } catch (FreeColException e) { SwingUtilities.invokeLater(new Runnable() { public void run() { gui.closeMainPanel(); gui.showMainPanel(); } }); SwingUtilities.invokeLater(new ErrorJob(e.getMessage())); logger.log(Level.WARNING, "FreeCol error starting game.", e); } } }; freeColClient.worker.schedule(loadGameJob); } /** * Sends a logout message to the server. * * @param notifyServer Whether or not the server should be * notified of the logout. For example: if the server kicked us * out then we don't need to confirm with a logout message. */ public void logout(boolean notifyServer) { if (notifyServer) { freeColClient.askServer().logout(); } freeColClient.askServer().disconnect(); ResourceManager.setScenarioMapping(null); ResourceManager.setCampaignMapping(null); if (!freeColClient.isHeadless()) { freeColClient.setInGame(false); } freeColClient.setGame(null); freeColClient.setMyPlayer(null); freeColClient.askServer().reset(); freeColClient.setLoggedIn(false); } /** * Quits the current game, optionally notifying and stopping the server. * * @param stopServer Whether to stop the server. * @param notifyServer Whether or not the server should be * notified of the logout. For example: if the server kicked us * out then we don't need to confirm with a logout message. */ public void quitGame(boolean stopServer, boolean notifyServer) { if (freeColClient.isLoggedIn()) logout(notifyServer); final FreeColServer server = freeColClient.getFreeColServer(); if (stopServer && server != null) { server.getController().shutdown(); freeColClient.setFreeColServer(null); } } /** * Quits the current game. If a server is running it will be * stopped if stopServer is <i>true</i>. The server and perhaps * the clients (if a server is running through this client and * stopServer is true) will be notified. * * @param stopServer Indicates whether or not a server that was * started through this client should be stopped. */ public void quitGame(boolean stopServer) { quitGame(stopServer, true); } /** * Returns a list of vacant players on a given server. * * @param host The name of the machine running the * <code>FreeColServer</code>. * @param port The port to use when connecting to the host. * @return A list of available {@link Player#getName() user names}. */ private List<String> getVacantPlayers(String host, int port) { Connection mc; try { mc = new Connection(host, port, null, FreeCol.CLIENT_THREAD); } catch (IOException e) { logger.log(Level.WARNING, "Could not connect to server.", e); return null; } List<String> items = new ArrayList<String>(); Element element = DOMMessage.createMessage("getVacantPlayers"); try { Element reply = mc.ask(element); if (reply == null) { logger.warning("The server did not return a list."); return null; } else if (!reply.getTagName().equals("vacantPlayers")) { logger.warning("The reply has an unknown type: " + reply.getTagName()); return null; } NodeList nl = reply.getChildNodes(); for (int i = 0; i < nl.getLength(); i++) { items.add(((Element)nl.item(i)).getAttribute("username")); } } catch (IOException e) { logger.log(Level.WARNING, "Could not send message to server.", e); } finally { mc.close(); } return items; } /** * Gets a list of servers from the meta server. * * @return A list of {@link ServerInfo} objects. */ public List<ServerInfo> getServerList() { Connection mc; try { mc = new Connection(FreeCol.META_SERVER_ADDRESS, FreeCol.META_SERVER_PORT, null, FreeCol.CLIENT_THREAD); } catch (IOException e) { gui.errorMessage("metaServer.couldNotConnect"); logger.log(Level.WARNING, "Could not connect to meta-server.", e); return null; } try { Element reply = mc.ask(DOMMessage.createMessage("getServerList")); if (reply == null) { gui.errorMessage("metaServer.communicationError"); logger.warning("The meta-server did not return a list."); return null; } else { List<ServerInfo> items = new ArrayList<ServerInfo>(); NodeList nl = reply.getChildNodes(); for (int i = 0; i < nl.getLength(); i++) { items.add(new ServerInfo((Element)nl.item(i))); } return items; } } catch (IOException e) { gui.errorMessage("metaServer.communicationError"); logger.log(Level.WARNING, "Network error with meta-server.", e); return null; } finally { mc.close(); } } }