/** * 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.io.PrintWriter; import java.io.StringWriter; import java.net.ConnectException; import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; import org.freecolandroid.repackaged.javax.swing.SwingUtilities; import org.freecolandroid.xml.stream.XMLStreamException; import org.freecolandroid.xml.stream.XMLStreamReader; import org.freecolandroid.xml.stream.XMLStreamWriter; 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.client.networking.Client; import net.sf.freecol.common.FreeColException; import net.sf.freecol.common.ServerInfo; 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.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 controller. */ public ConnectController(FreeColClient freeColClient, GUI gui) { this.freeColClient = freeColClient; this.gui = gui; } /** * Starts a multiplayer server and connects to it. * * @param username The name to use when logging in. * @param port The port in which the server should listen for new clients. * @param level a <code>DifficultyLevel</code> value */ public void startMultiplayerGame(Specification specification, boolean publicServer, String username, int port, Advantages advantages, OptionGroup level) { freeColClient.setMapEditor(false); if (freeColClient.isLoggedIn()) { logout(true); } if (freeColClient.getFreeColServer() != null && freeColClient.getFreeColServer().getServer().getPort() == port) { if (gui.showConfirmDialog("stopServer.text", "stopServer.yes", "stopServer.no")) { freeColClient.getFreeColServer().getController().shutdown(); } else { return; } } try { FreeColServer freeColServer = new FreeColServer(specification, publicServer, false, port, null, advantages); freeColClient.setFreeColServer(freeColServer); } catch (NoRouteToServerException e) { gui.errorMessage("server.noRouteToServer"); return; } catch (IOException e) { gui.errorMessage("server.couldNotStart"); return; } joinMultiplayerGame(username, "localhost", port); } /** * Starts a new singleplayer game by connecting to the server. * * @param specification a <code>Specification</code> value * @param username The name to use when logging in. * @param advantages an <code>Advantages</code> value */ public void startSingleplayerGame(Specification specification, String username, Advantages advantages) { System.out.println("ConnectController.startSingleplayerGame()"); freeColClient.setMapEditor(false); if (freeColClient.isLoggedIn()) { logout(true); } // TODO: connect client/server directly (not using network-classes) int port = FreeCol.getDefaultPort(); if (freeColClient.getFreeColServer() != null && freeColClient.getFreeColServer().getServer().getPort() == port) { if (gui.showConfirmDialog("stopServer.text", "stopServer.yes", "stopServer.no")) { freeColClient.getFreeColServer().getController().shutdown(); } else { return; } } loadModFragments(specification); try { FreeColServer freeColServer = new FreeColServer(specification, false, true, port, null, advantages); if (freeColClient.getClientOptions().getBoolean(ClientOptions.AUTOSAVE_DELETE)) { FreeColServer.removeAutosaves(Messages.message("clientOptions.savegames.autosave.fileprefix")); } freeColClient.setFreeColServer(freeColServer); } catch (NoRouteToServerException e) { logger.warning("Illegal state: An exception occured that can only appear in public multiplayer games."); return; } catch (IOException e) { gui.errorMessage("server.couldNotStart"); return; } freeColClient.setSingleplayer(true); if (login(username, "127.0.0.1", port)) { 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 <code>FreeColServer</code>. * @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) { username = choice; } else { return; } } 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 a <code>boolean</code> value */ public boolean login(String username, String host, int port) { System.out.println("ConnectController.login() user=" + username + ", host=" + host); Client client = freeColClient.getClient(); freeColClient.setMapEditor(false); if (client != null) { client.disconnect(); } try { client = new Client(host, port, freeColClient.getPreGameInputHandler(), FreeCol.CLIENT_THREAD + username); } catch (ConnectException e) { gui.errorMessage("server.couldNotConnect"); return false; } catch (IOException e) { gui.errorMessage("server.couldNotConnect"); return false; } freeColClient.setClient(client); Connection c = client.getConnection(); XMLStreamReader in = null; try { System.out.println("Sending login"); XMLStreamWriter out = c.ask(); out.writeStartElement("login"); out.writeAttribute("username", username); out.writeAttribute("freeColVersion", FreeCol.getVersion()); out.writeEndElement(); in = c.getReply(); System.out.println("Got reply=" + in.getLocalName()); if (in.getLocalName().equals("loginConfirmed")) { final String startGameStr = in.getAttributeValue(null, "startGame"); boolean startGame = (startGameStr != null) && Boolean.valueOf(startGameStr).booleanValue(); boolean singleplayer = Boolean.valueOf(in.getAttributeValue(null, "singleplayer")).booleanValue(); boolean isCurrentPlayer = Boolean.valueOf(in.getAttributeValue(null, "isCurrentPlayer")).booleanValue(); String activeUnitId = in.getAttributeValue(null, "activeUnit"); in.nextTag(); Game game = new Game(in, username); // 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 Player thisPlayer = game.getPlayerByName(username); freeColClient.setGame(game); freeColClient.setMyPlayer(thisPlayer); freeColClient.getActionManager().addSpecificationActions(game.getSpecification()); c.endTransmission(in); // If (true) --> reconnect if (startGame) { Tile entryTile = thisPlayer.getEntryLocation().getTile(); freeColClient.setSingleplayer(singleplayer); freeColClient.getPreGameController().startGame(); if (isCurrentPlayer) { freeColClient.getInGameController() .setCurrentPlayer(thisPlayer); if (activeUnitId != null) { Unit active = (Unit) freeColClient.getGame().getFreeColGameObject(activeUnitId); if (active != null) { active.getOwner().resetIterators(); active.getOwner().setNextActiveUnit(active); gui.setActiveUnit(active); } } else { gui.setSelectedTile(entryTile, false); } } else { gui.setSelectedTile(entryTile, false); } gui.setSelectedTile(thisPlayer .getEntryLocation().getTile(), false); } } else if (in.getLocalName().equals("error")) { gui.errorMessage(in.getAttributeValue(null, "messageID"), in.getAttributeValue(null, "message")); c.endTransmission(in); return false; } else { logger.warning("Unkown message received: " + in.getLocalName()); c.endTransmission(in); return false; } } catch (Exception e) { StringWriter sw = new StringWriter(); e.printStackTrace(new PrintWriter(sw)); logger.warning(sw.toString()); gui.errorMessage(null, "Could not send XML to the server."); try { c.endTransmission(in); } catch (IOException ie) { logger.warning("Exception while trying to end transmission: " + ie.toString()); } } 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(FreeCol.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 "public game" settings from the file: final FreeColSavegameFile fis = new FreeColSavegameFile(theFile); xs = new XMLStream(fis.getSavegameInputStream()); final XMLStreamReader in = xs.getXMLStreamReader(); in.nextTag(); final boolean defaultSingleplayer = Boolean.valueOf(in.getAttributeValue(null, "singleplayer")).booleanValue(); final boolean defaultPublicServer; final String publicServerStr = in.getAttributeValue(null, "publicServer"); if (publicServerStr != null) { defaultPublicServer = Boolean.valueOf(publicServerStr).booleanValue(); } else { defaultPublicServer = false; } 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); if (sgo == ClientOptions.SHOW_SAVEGAME_SETTINGS_ALWAYS || !defaultSingleplayer && sgo == ClientOptions.SHOW_SAVEGAME_SETTINGS_MULTIPLAYER) { 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 = FreeCol.getDefaultPort(); } } catch (FileNotFoundException e) { SwingUtilities.invokeLater( new ErrorJob("fileNotFound") ); return; } catch (IOException e) { SwingUtilities.invokeLater( new ErrorJob("server.couldNotStart") ); return; } catch (NullPointerException e) { SwingUtilities.invokeLater( new ErrorJob("couldNotLoadGame") ); return; } catch (XMLStreamException e) { StringWriter sw = new StringWriter(); e.printStackTrace(new PrintWriter(sw)); logger.warning(sw.toString()); SwingUtilities.invokeLater( new ErrorJob("server.couldNotStart") ); return; } finally { if (xs != null) { xs.close(); } } if (freeColClient.getFreeColServer() != null && freeColClient.getFreeColServer().getServer().getPort() == port) { if (gui.showConfirmDialog("stopServer.text", "stopServer.yes", "stopServer.no")) { freeColClient.getFreeColServer().getController().shutdown(); } else { return; } } gui.showStatusPanel(Messages.message("status.loadingGame"), true); 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(); freeColClient.setSingleplayer(singleplayer); freeColClient.getInGameController().setGameConnected(); SwingUtilities.invokeLater( new Runnable() { public void run() { ResourceManager.setScenarioMapping(savegame.getResourceMapping()); login(username, "127.0.0.1", FreeCol.getDefaultPort()); gui.closeStatusPanel(); } } ); } catch (NoRouteToServerException e) { SwingUtilities.invokeLater(new Runnable() { public void run() { gui.closeMainPanel(); gui.showMainPanel(); } }); SwingUtilities.invokeLater( new ErrorJob("server.noRouteToServer") ); } catch (FileNotFoundException e) { SwingUtilities.invokeLater(new Runnable() { public void run() { gui.closeMainPanel(); gui.showMainPanel(); } }); SwingUtilities.invokeLater( new ErrorJob("fileNotFound") ); } catch (IOException e) { SwingUtilities.invokeLater(new Runnable() { public void run() { gui.closeMainPanel(); gui.showMainPanel(); } }); SwingUtilities.invokeLater( new ErrorJob("server.couldNotStart") ); } catch (FreeColException e) { SwingUtilities.invokeLater(new Runnable() { public void run() { gui.closeMainPanel(); gui.showMainPanel(); } }); SwingUtilities.invokeLater( new ErrorJob(e.getMessage()) ); } } }; 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) { Element logoutMessage = DOMMessage.createNewRootElement("logout"); logoutMessage.setAttribute("reason", "User has quit the client."); freeColClient.getClient().sendAndWait(logoutMessage); } try { freeColClient.getClient().getConnection().close(); } catch (IOException e) { logger.warning("Could not close connection!"); } ResourceManager.setScenarioMapping(null); ResourceManager.setCampaignMapping(null); if (!freeColClient.isHeadless()) { freeColClient.setInGame(false); } freeColClient.setGame(null); freeColClient.setMyPlayer(null); freeColClient.setClient(null); freeColClient.setLoggedIn(false); } /** * Quits the current game. If a server is running it will be stopped if bStopServer is * <i>true</i>. * If a server is running through this client and bStopServer is true then the clients * connected to that server will be notified. If a local client is connected to a server * then the server will be notified with a logout in case <i>notifyServer</i> is true. * * @param bStopServer Indicates whether or not a server that was started through this * client should be stopped. * * @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 bStopServer, boolean notifyServer) { final FreeColServer server = freeColClient.getFreeColServer(); if (bStopServer && server != null) { server.getController().shutdown(); freeColClient.setFreeColServer(null); ResourceManager.setScenarioMapping(null); ResourceManager.setCampaignMapping(null); freeColClient.setInGame(false); freeColClient.setGame(null); freeColClient.setMyPlayer(null); freeColClient.setIsRetired(false); freeColClient.setClient(null); freeColClient.setLoggedIn(false); } else if (freeColClient.isLoggedIn()) { logout(notifyServer); } } /** * Quits the current game. If a server is running it will be stopped if bStopServer is * <i>true</i>. * The server and perhaps the clients (if a server is running through this client and * bStopServer is true) will be notified. * * @param bStopServer Indicates whether or not a server that was started through this * client should be stopped. */ public void quitGame(boolean bStopServer) { quitGame(bStopServer, 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() usernames}. */ private List<String> getVacantPlayers(String host, int port) { Connection mc; try { mc = new Connection(host, port, null, FreeCol.CLIENT_THREAD); } catch (IOException e) { logger.warning("Could not connect to server."); return null; } ArrayList<String> items = new ArrayList<String>(); Element element = DOMMessage.createNewRootElement("getVacantPlayers"); try { Element reply = mc.askDumping(element); if (reply == null) { logger.warning("The server did not return a list."); return null; } 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.warning("Could not send message to server."); } finally { try { mc.close(); } catch (IOException e) { logger.warning("Could not close connection."); } } return items; } /** * Gets a list of servers from the meta server. * @return A list of {@link ServerInfo} objects. */ public ArrayList<ServerInfo> getServerList() { Connection mc; try { mc = new Connection(FreeCol.META_SERVER_ADDRESS, FreeCol.META_SERVER_PORT, null, FreeCol.CLIENT_THREAD); } catch (IOException e) { logger.warning("Could not connect to meta-server."); gui.errorMessage("metaServer.couldNotConnect"); return null; } try { Element gslElement = DOMMessage.createNewRootElement("getServerList"); Element reply = mc.askDumping(gslElement); if (reply == null) { logger.warning("The meta-server did not return a list."); gui.errorMessage("metaServer.communicationError"); return null; } else { ArrayList<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) { logger.warning("Network error while communicating with the meta-server."); gui.errorMessage("metaServer.communicationError"); return null; } finally { try { mc.close(); } catch (IOException e) { logger.warning("Could not close connection to meta-server."); return null; } } } 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.warning("IO error in mod fragment " + f.getId() + ": " + ioe.getMessage()); } if (sis != null) { try { specification.loadFragment(sis); loadedMod = true; logger.info("Loaded mod fragment " + f.getId()); } catch (RuntimeException rte) { logger.warning("Parse error in mod fragment " + f.getId() + ": " + rte.getMessage()); } } } if (loadedMod) { // Update actions in case new ones loaded. freeColClient.getActionManager().update(); } } }