/**
* 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();
}
}
}