/**
* 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.server;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.freecol.FreeCol;
import net.sf.freecol.common.FreeColException;
import net.sf.freecol.common.io.FreeColSavegameFile;
import net.sf.freecol.common.io.FreeColTcFile;
import net.sf.freecol.common.model.Building;
import net.sf.freecol.common.model.Colony;
import net.sf.freecol.common.model.ColonyTile;
import net.sf.freecol.common.model.Europe;
import net.sf.freecol.common.model.FreeColGameObject;
import net.sf.freecol.common.model.FreeColObject;
import net.sf.freecol.common.model.Game;
import net.sf.freecol.common.model.GameOptions;
import net.sf.freecol.common.model.HighScore;
import net.sf.freecol.common.model.IndianSettlement;
import net.sf.freecol.common.model.Modifier;
import net.sf.freecol.common.model.Nation;
import net.sf.freecol.common.model.NationOptions;
import net.sf.freecol.common.model.NationOptions.Advantages;
import net.sf.freecol.common.model.Player;
import net.sf.freecol.common.model.Settlement;
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.BooleanOption;
import net.sf.freecol.common.option.IntegerOption;
import net.sf.freecol.common.option.OptionGroup;
import net.sf.freecol.common.option.StringOption;
import net.sf.freecol.common.util.XMLStream;
import net.sf.freecol.server.ai.AIInGameInputHandler;
import net.sf.freecol.server.ai.AIMain;
import net.sf.freecol.server.ai.AIPlayer;
import net.sf.freecol.server.control.Controller;
import net.sf.freecol.server.control.InGameController;
import net.sf.freecol.server.control.InGameInputHandler;
import net.sf.freecol.server.control.PreGameController;
import net.sf.freecol.server.control.PreGameInputHandler;
import net.sf.freecol.server.control.UserConnectionHandler;
import net.sf.freecol.server.generator.MapGenerator;
import net.sf.freecol.server.generator.SimpleMapGenerator;
import net.sf.freecol.server.generator.TerrainGenerator;
import net.sf.freecol.server.model.ServerGame;
import net.sf.freecol.server.model.ServerModelObject;
import net.sf.freecol.server.model.ServerPlayer;
import net.sf.freecol.server.model.TransactionSession;
import net.sf.freecol.server.networking.DummyConnection;
import net.sf.freecol.server.networking.Server;
import org.freecolandroid.repackaged.java.awt.image.BufferedImage;
import org.freecolandroid.repackaged.javax.imageio.ImageIO;
import org.freecolandroid.xml.stream.XMLInputFactory;
import org.freecolandroid.xml.stream.XMLOutputFactory;
import org.freecolandroid.xml.stream.XMLStreamConstants;
import org.freecolandroid.xml.stream.XMLStreamException;
import org.freecolandroid.xml.stream.XMLStreamReader;
import org.freecolandroid.xml.stream.XMLStreamWriter;
import org.w3c.dom.Element;
/**
* The main control class for the FreeCol server. This class both starts and
* keeps references to all of the server objects and the game model objects.
* <br>
* <br>
* If you would like to start a new server you just create a new object of this
* class.
*/
public final class FreeColServer {
private static final Logger logger = Logger.getLogger(FreeColServer.class.getName());
private static final int META_SERVER_UPDATE_INTERVAL = 60000;
private static final int NUMBER_OF_HIGH_SCORES = 10;
private static final String HIGH_SCORE_FILE = "HighScores.xml";
/** Hex constant digits for get/restoreRandomState. */
private static final String HEX_DIGITS = "0123456789ABCDEF";
/**
* The save game format used for saving games.
*
* Version 7-10 were used in 0.9.x.
* Version 11 made a lot of changes and was introduced for the 0.10.0
* series.
* Version 12 was introduced with HighSeas post-0.10.1.
*
* Please add to this comment if you increase the version.
*/
public static final int SAVEGAME_VERSION = 12;
/**
* The oldest save game format that can still be loaded.
* The promise is that FreeCol 0.n.* can load 0.(n-1).* games.
*
* TODO: revisit the numbering scheme and save compatibility promise
* when 1.0 is released.
*/
public static final int MINIMUM_SAVEGAME_VERSION = 7;
/**
* The specification to use when loading old format games where
* a spec may not be readily available.
*/
public static final String defaultSpec = "freecol";
/** Constant for storing the state of the game. */
public static enum GameState {STARTING_GAME, IN_GAME, ENDING_GAME}
/** Stores the current state of the game. */
private GameState gameState = GameState.STARTING_GAME;
// Networking:
private Server server;
// Control:
private final UserConnectionHandler userConnectionHandler;
private final PreGameController preGameController;
private final PreGameInputHandler preGameInputHandler;
private final InGameInputHandler inGameInputHandler;
private final InGameController inGameController;
private ServerGame game;
private AIMain aiMain;
private MapGenerator mapGenerator;
private boolean singleplayer;
// The username of the player owning this server.
private String owner;
private boolean publicServer = false;
private final int port;
/** The name of this server. */
private String name;
/** The private provider for random numbers. */
private Random random = null;
/** Did the integrity check succeed */
private boolean integrity = false;
/** An active unit specified in a saved game. */
private Unit activeUnit = null;
/**
* The high scores on this server.
*/
private List<HighScore> highScores = null;
public static final Comparator<HighScore> highScoreComparator
= new Comparator<HighScore>() {
public int compare(HighScore score1, HighScore score2) {
return score2.getScore() - score1.getScore();
}
};
/**
* Starts a new server in a specified mode and with a specified port.
*
* @param publicServer This value should be set to <code>true</code> in
* order to appear on the meta server's listing.
*
* @param singleplayer Sets the game as singleplayer (if <i>true</i>) or
* multiplayer (if <i>false</i>).
*
* @param port The TCP port to use for the public socket. That is the port
* the clients will connect to.
*
* @param name The name of the server, or <code>null</code> if the default
* name should be used.
*
* @throws IOException if the public socket cannot be created (the exception
* will be logged by this class).
*
*/
public FreeColServer(Specification specification, boolean publicServer,
boolean singleplayer, int port, String name)
throws IOException, NoRouteToServerException {
this(specification, publicServer, singleplayer, port, name,
Advantages.SELECTABLE);
}
public FreeColServer(Specification specification, boolean publicServer,
boolean singleplayer, int port, String name,
Advantages advantages)
throws IOException, NoRouteToServerException {
this.publicServer = publicServer;
this.singleplayer = singleplayer;
this.port = port;
this.name = name;
this.random = new Random(FreeCol.getFreeColSeed());
userConnectionHandler = new UserConnectionHandler(this);
preGameController = new PreGameController(this);
preGameInputHandler = new PreGameInputHandler(this);
inGameInputHandler = new InGameInputHandler(this);
inGameController = new InGameController(this, random);
game = new ServerGame(specification);
game.setNationOptions(new NationOptions(specification, advantages));
// @compat 0.9.x, 0.10.x
fixGameOptions();
// end compatibility code
mapGenerator = new SimpleMapGenerator(random, specification);
try {
server = new Server(this, port);
server.start();
} catch (IOException e) {
logger.warning("Exception while starting server: " + e);
throw e;
}
updateMetaServer(true);
startMetaServerUpdateThread();
}
/**
* Starts a new server in a specified mode and with a specified port and
* loads the game from the given file.
*
* @param savegame The file where the game data is located.
*
* @param port The TCP port to use for the public socket. That is the port
* the clients will connect to.
*
* @param name The name of the server, or <code>null</code> if the default
* name should be used.
*
* @exception IOException if the public socket cannot be created (the exception
* will be logged by this class).
*
* @exception FreeColException if the savegame could not be loaded.
* @exception NoRouteToServerException if an error occurs
*/
public FreeColServer(final FreeColSavegameFile savegame, int port, String name)
throws IOException, FreeColException, NoRouteToServerException {
this(savegame, port, name, null);
}
/**
* Starts a new server in a specified mode and with a specified port and
* loads the game from the given file.
*
* @param savegame The file where the game data is located.
*
* @param port The TCP port to use for the public socket. That is the port
* the clients will connect to.
*
* @param name The name of the server, or <code>null</code> if the default
* name should be used.
*
* @param specification a <code>Specification</code> value
* @exception IOException if the public socket cannot be created (the exception
* will be logged by this class).
*
* @exception FreeColException if the savegame could not be loaded.
* @exception NoRouteToServerException if an error occurs
*/
public FreeColServer(final FreeColSavegameFile savegame, int port,
String name, Specification specification)
throws IOException, FreeColException, NoRouteToServerException {
this.port = port;
this.name = name;
//this.nationOptions = nationOptions;
mapGenerator = null;
userConnectionHandler = new UserConnectionHandler(this);
preGameController = new PreGameController(this);
preGameInputHandler = new PreGameInputHandler(this);
inGameInputHandler = new InGameInputHandler(this);
try {
server = new Server(this, port);
server.start();
} catch (IOException e) {
logger.warning("Exception while starting server: " + e);
throw e;
}
try {
loadGame(savegame, specification);
} catch (FreeColException e) {
server.shutdown();
throw e;
} catch (Exception e) {
server.shutdown();
FreeColException fe = new FreeColException("couldNotLoadGame");
fe.initCause(e);
throw fe;
}
if (random == null) {
this.random = new Random(FreeCol.getFreeColSeed());
}
inGameController = new InGameController(this, random);
mapGenerator = new SimpleMapGenerator(random, getSpecification());
updateMetaServer(true);
startMetaServerUpdateThread();
TransactionSession.clearAll();
}
/**
* Sets the mode of the game: singleplayer/multiplayer.
*
* @param singleplayer Sets the game as singleplayer (if <i>true</i>) or
* multiplayer (if <i>false</i>).
*/
public void setSingleplayer(boolean singleplayer) {
this.singleplayer = singleplayer;
}
/**
* Checks if the user is playing in singleplayer mode.
*
* @return <i>true</i> if the user is playing in singleplayer mode,
* <i>false</i> otherwise.
*/
public boolean isSingleplayer() {
return singleplayer;
}
/**
* Gets the specification from the game run by this server.
*
* @return The specification from the game.
*/
public Specification getSpecification() {
return game.getSpecification();
}
/**
* Gets the <code>UserConnectionHandler</code>.
*
* @return The <code>UserConnectionHandler</code> that is beeing used when
* new client connect.
*/
public UserConnectionHandler getUserConnectionHandler() {
return userConnectionHandler;
}
/**
* Gets the <code>Controller</code>.
*
* @return The <code>Controller</code>.
*/
public Controller getController() {
if (getGameState() == GameState.IN_GAME) {
return inGameController;
} else {
return preGameController;
}
}
/**
* Gets the <code>PreGameInputHandler</code>.
*
* @return The <code>PreGameInputHandler</code>.
*/
public PreGameInputHandler getPreGameInputHandler() {
return preGameInputHandler;
}
/**
* Gets the <code>InGameInputHandler</code>.
*
* @return The <code>InGameInputHandler</code>.
*/
public InGameInputHandler getInGameInputHandler() {
return inGameInputHandler;
}
/**
* Gets the controller being used while the game is running.
*
* @return The controller from making a new turn etc.
*/
public InGameController getInGameController() {
return inGameController;
}
/**
* Gets the <code>Game</code> that is being played.
*
* @return The <code>Game</code> which is the main class of the game-model
* being used in this game.
*/
public ServerGame getGame() {
return game;
}
/**
* Sets the <code>Game</code> that is being played.
*
* @param game The new <code>Game</code>.
*/
public void setGame(ServerGame game) {
this.game = game;
}
/**
* Sets the main AI-object.
*
* @param aiMain The main AI-object which is responsible for controlling,
* updating and saving the AI objects.
*/
public void setAIMain(AIMain aiMain) {
this.aiMain = aiMain;
}
/**
* Gets the main AI-object.
*
* @return The main AI-object which is responsible for controlling, updating
* and saving the AI objects.
*/
public AIMain getAIMain() {
return aiMain;
}
/**
* Gets the current state of the game.
*
* @return One of: {@link GameState#STARTING_GAME}, {@link GameState#IN_GAME} and
* {@link GameState#ENDING_GAME}.
*/
public GameState getGameState() {
return gameState;
}
/**
* Sets the current state of the game.
*
* @param state The new state to be set. One of: {@link GameState#STARTING_GAME},
* {@link GameState#IN_GAME} and {@link GameState#ENDING_GAME}.
*/
public void setGameState(GameState state) {
gameState = state;
}
/**
* Gets the network server responsible of handling the connections.
*
* @return The network server.
*/
public Server getServer() {
return server;
}
/**
* Gets the integrity check result.
*
* @return The integrity check result.
*/
public boolean getIntegrity() {
return integrity;
}
/**
* Gets the server random number generator.
*
* @return The server random number generator.
*/
public Random getServerRandom() {
return random;
}
/**
* Sets the server random number generator.
*
* @param random The new random number generator.
*/
public void setServerRandom(Random random) {
this.random = random;
}
/**
* Gets the <code>MapGenerator</code> this <code>FreeColServer</code> is
* using when creating random maps.
*
* @return The <code>MapGenerator</code>.
*/
public MapGenerator getMapGenerator() {
return mapGenerator;
}
/**
* Sets the <code>MapGenerator</code> this <code>FreeColServer</code> is
* using when creating random maps.
*
* @param mapGenerator The <code>MapGenerator</code>.
*/
public void setMapGenerator(MapGenerator mapGenerator) {
this.mapGenerator = mapGenerator;
}
/**
* Sends information about this server to the meta-server. The information
* is only sent if <code>public == true</code>.
*/
public void updateMetaServer() throws NoRouteToServerException {
updateMetaServer(false);
}
/**
* Returns the name of this server.
*
* @return The name.
*/
public String getName() {
return name;
}
/**
* Sets the name of this server.
*
* @param name The name.
*/
public void setName(String name) {
this.name = name;
}
/**
* Gets the owner of the <code>Game</code>.
*
* @return The owner of the game.
*/
public String getOwner() {
return owner;
}
/**
* Sets the owner of the game.
*
* @param owner The new owner of the game.
*/
public void setOwner(String owner) {
this.owner = owner;
}
/**
* Gets the active unit specified in a saved game, if any.
*
* @return The active unit.
*/
public Unit getActiveUnit() {
return activeUnit;
}
/**
* Gets the active unit specified in a saved game, if any.
*
* @param unit The active unit to save.
*/
public void setActiveUnit(Unit unit) {
activeUnit = unit;
}
/**
* Sets the public server.
*
* @param publicServer The new public server flag.
*/
public void setPublicServer(boolean publicServer) {
this.publicServer = publicServer;
}
/**
* Starts the metaserver update thread if <code>publicServer == true</code>.
*
* This update is really a "Hi! I am still here!"-message, since an
* additional update should be sent when a new player is added to/removed
* from this server etc.
*/
public void startMetaServerUpdateThread() {
if (!publicServer) {
return;
}
Timer t = new Timer(true);
t.scheduleAtFixedRate(new TimerTask() {
public void run() {
try {
updateMetaServer();
} catch (NoRouteToServerException e) {}
}
}, META_SERVER_UPDATE_INTERVAL, META_SERVER_UPDATE_INTERVAL);
}
/**
* Sends information about this server to the meta-server. The information
* is only sent if <code>public == true</code>.
*
* @param firstTime Should be set to <i>true></i> when calling this method
* for the first time.
* @throws NoRouteToServerException if the meta-server cannot connect to
* this server.
*/
public void updateMetaServer(boolean firstTime) throws NoRouteToServerException {
if (!publicServer) {
return;
}
Connection mc;
try {
mc = new Connection(FreeCol.META_SERVER_ADDRESS, FreeCol.META_SERVER_PORT, null, FreeCol.SERVER_THREAD);
} catch (IOException e) {
logger.warning("Could not connect to meta-server.");
return;
}
try {
Element element;
if (firstTime) {
element = DOMMessage.createNewRootElement("register");
} else {
element = DOMMessage.createNewRootElement("update");
}
// TODO: Add possibility of choosing a name:
if (name != null) {
element.setAttribute("name", name);
} else {
element.setAttribute("name", mc.getSocket().getLocalAddress().getHostAddress() + ":"
+ Integer.toString(port));
}
element.setAttribute("port", Integer.toString(port));
element.setAttribute("slotsAvailable", Integer.toString(getSlotsAvailable()));
element.setAttribute("currentlyPlaying", Integer.toString(getNumberOfLivingHumanPlayers()));
element.setAttribute("isGameStarted", Boolean.toString(gameState != GameState.STARTING_GAME));
element.setAttribute("version", FreeCol.getVersion());
element.setAttribute("gameState", Integer.toString(getGameState().ordinal()));
Element reply = mc.askDumping(element);
if (reply != null && reply.getTagName().equals("noRouteToServer")) {
throw new NoRouteToServerException();
}
} catch (IOException e) {
logger.warning("Network error while communicating with the meta-server.");
return;
} finally {
try {
// mc.reallyClose();
mc.close();
} catch (IOException e) {
logger.warning("Could not close connection to meta-server.");
return;
}
}
}
/**
* Removes this server from the metaserver's list. The information is only
* sent if <code>public == true</code>.
*/
public void removeFromMetaServer() {
if (!publicServer) {
return;
}
Connection mc;
try {
mc = new Connection(FreeCol.META_SERVER_ADDRESS, FreeCol.META_SERVER_PORT, null, FreeCol.SERVER_THREAD);
} catch (IOException e) {
logger.warning("Could not connect to meta-server.");
return;
}
try {
Element element = DOMMessage.createNewRootElement("remove");
element.setAttribute("port", Integer.toString(port));
mc.send(element);
} catch (IOException e) {
logger.warning("Network error while communicating with the meta-server.");
return;
} finally {
try {
// mc.reallyClose();
mc.close();
} catch (IOException e) {
logger.warning("Could not close connection to meta-server.");
return;
}
}
}
/**
* Gets the number of player that may connect.
*
* @return The number of available slots for human players. This number also
* includes european players currently controlled by the AI.
*/
public int getSlotsAvailable() {
List<Player> players = game.getPlayers();
int n = 0;
for (int i = 0; i < players.size(); i++) {
ServerPlayer p = (ServerPlayer) players.get(i);
if (!p.isEuropean() || p.isREF()) {
continue;
}
if (!(p.isDead() || p.isConnected() && !p.isAI())) {
n++;
}
}
return n;
}
/**
* Gets the number of human players in this game that is still playing.
*
* @return The number.
*/
public int getNumberOfLivingHumanPlayers() {
List<Player> players = game.getPlayers();
int n = 0;
for (int i = 0; i < players.size(); i++) {
if (!((ServerPlayer) players.get(i)).isAI() && !((ServerPlayer) players.get(i)).isDead()
&& ((ServerPlayer) players.get(i)).isConnected()) {
n++;
}
}
return n;
}
/**
* Saves a game.
*
* @param file The file where the data will be written.
* @param username The username of the player saving the game.
* @throws IOException If a problem was encountered while trying to open,
* write or close the file.
*/
public void saveGame(File file, String username, OptionGroup options) throws IOException {
saveGame(file, username, options, null);
}
/**
* Saves a game.
*
* @param file The file where the data will be written.
* @param username The username of the player saving the game.
* @param image an <code>Image</code> value
* @exception IOException If a problem was encountered while trying to open,
* write or close the file.
*/
public void saveGame(File file, String username, OptionGroup options, BufferedImage image)
throws IOException {
final ServerGame game = getGame();
XMLOutputFactory xof = XMLOutputFactory.newInstance();
JarOutputStream fos = null;
try {
XMLStreamWriter xsw;
fos = new JarOutputStream(new FileOutputStream(file));
if (image != null) {
fos.putNextEntry(new JarEntry(FreeColSavegameFile.THUMBNAIL_FILE));
ImageIO.write(image, "png", fos);
fos.closeEntry();
}
if (options != null) {
fos.putNextEntry(new JarEntry(FreeColSavegameFile.CLIENT_OPTIONS));
options.save(fos);
fos.closeEntry();
}
Properties properties = new Properties();
properties.put("map.width", Integer.toString(game.getMap().getWidth()));
properties.put("map.height", Integer.toString(game.getMap().getHeight()));
fos.putNextEntry(new JarEntry(FreeColSavegameFile.SAVEGAME_PROPERTIES));
properties.store(fos, null);
fos.closeEntry();
// save the actual game data
fos.putNextEntry(new JarEntry(FreeColSavegameFile.SAVEGAME_FILE));
xsw = xof.createXMLStreamWriter(fos, "UTF-8");
xsw.writeStartDocument("UTF-8", "1.0");
xsw.writeComment("Game version: "+FreeCol.getRevision());
xsw.writeStartElement("savedGame");
// Add the attributes:
xsw.writeAttribute("owner", username);
xsw.writeAttribute("publicServer", Boolean.toString(publicServer));
xsw.writeAttribute("singleplayer", Boolean.toString(singleplayer));
xsw.writeAttribute("version", Integer.toString(SAVEGAME_VERSION));
xsw.writeAttribute("randomState", getRandomState(random));
if (getActiveUnit() != null) {
xsw.writeAttribute("activeUnit", getActiveUnit().getId());
}
// Add server side model information:
xsw.writeStartElement("serverObjects");
for (ServerModelObject smo : game.getServerModelObjects()) {
xsw.writeStartElement(smo.getServerXMLElementTagName());
xsw.writeAttribute(FreeColObject.ID_ATTRIBUTE,
((FreeColGameObject) smo).getId());
xsw.writeEndElement();
}
xsw.writeEndElement();
// Add the game:
game.toXML(xsw, null, true, true);
// Add the AIObjects:
if (aiMain != null) {
aiMain.toXML(xsw);
}
xsw.writeEndElement();
xsw.writeEndDocument();
xsw.flush();
xsw.close();
} catch (XMLStreamException e) {
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
logger.warning(sw.toString());
throw new IOException("XMLStreamException.");
} catch (Exception e) {
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
logger.warning(sw.toString());
throw new IOException(e.toString());
} finally {
try {
if (fos != null) {
fos.close();
}
} catch (IOException e) {
// do nothing
}
}
}
/**
* Creates a <code>XMLStream</code> for reading the given file.
* Compression is automatically detected.
*
* @param fis The file to be read.
* @return The <code>XMLStreamr</code>.
* @exception IOException if thrown while loading the game or if a
* <code>XMLStreamException</code> have been thrown by the
* parser.
*/
public static XMLStream createXMLStreamReader(FreeColSavegameFile fis) throws IOException {
return new XMLStream(fis.getSavegameInputStream());
}
/**
* Loads a game.
*
* @param fis The file where the game data is located.
* @return The username of the player saving the game.
* @throws IOException If a problem was encountered while trying to open,
* read or close the file.
* @exception IOException if thrown while loading the game or if a
* <code>XMLStreamException</code> have been thrown by the
* parser.
* @exception FreeColException if the savegame contains incompatible data.
*/
public Game loadGame(final FreeColSavegameFile fis)
throws IOException, FreeColException {
return loadGame(fis, null);
}
/**
* Reads just the game part from a save game.
* This routine exists apart from loadGame so that the map generator
* can load the predefined maps.
*
* @param fis The stream to read from.
* @param specification The <code>Specification</code> to use in the game.
* @param server Use this (optional) server to load into.
* @return The game found in the stream.
* @exception FreeColException if the format is incompatible.
* @exception IOException if there is a problem reading the stream.
*/
public static ServerGame readGame(final FreeColSavegameFile fis,
Specification specification,
FreeColServer server)
throws IOException, FreeColException {
final int savegameVersion = getSavegameVersion(fis);
ArrayList<String> serverStrings = null;
XMLStream xs = null;
try {
ServerGame game = null;
String active = null;
xs = createXMLStreamReader(fis);
final XMLStreamReader xsr = xs.getXMLStreamReader();
xsr.nextTag();
logger.info("Found savegame version " + savegameVersion);
if (server != null) {
server.setSingleplayer(FreeColObject.getAttribute(xsr,
"singleplayer", true));
server.setPublicServer(FreeColObject.getAttribute(xsr,
"publicServer", false));
String randomState = xsr.getAttributeValue(null, "randomState");
if (randomState != null && randomState.length() > 0) {
try {
server.setServerRandom(restoreRandomState(randomState));
} catch (IOException e) {
logger.log(Level.WARNING, "Failed to restore random state!", e);
}
}
server.setOwner(xsr.getAttributeValue(null, "owner"));
active = xsr.getAttributeValue(null, "activeUnit");
}
while (xsr.nextTag() != XMLStreamConstants.END_ELEMENT) {
if (xsr.getLocalName().equals("serverObjects")) {
serverStrings = new ArrayList<String>();
while (xsr.nextTag() != XMLStreamConstants.END_ELEMENT) {
serverStrings.add(xsr.getLocalName());
serverStrings.add(xsr.getAttributeValue(null,
FreeColObject.ID_ATTRIBUTE));
xsr.nextTag();
}
// @compat 0.9.x
if (savegameVersion < 11) {
v11FixServerObjects(serverStrings, fis);
}
// end compatibility code
} else if (xsr.getLocalName().equals(Game.getXMLElementTagName())) {
// @compat 0.9.x
if (savegameVersion < 9 && specification == null) {
specification = new FreeColTcFile(defaultSpec)
.getSpecification();
logger.info("Reading old format game"
+ " (version: " + savegameVersion
+ "), using " + defaultSpec + " specification.");
}
// end compatibility code
// Read the game
game = new ServerGame(null, xsr, serverStrings,
specification);
game.setCurrentPlayer(null);
if (server != null) server.setGame(game);
// @compat 0.9.x
if (savegameVersion < 9
&& game.getDifficultyLevel() == null) {
game.getSpecification().applyDifficultyLevel("model.difficulty.medium");
logger.info("Applying default difficulty of medium.");
}
// end compatibility code
} else if (xsr.getLocalName().equals(AIMain.getXMLElementTagName())) {
if (server == null) break;
server.setAIMain(new AIMain(server, xsr));
} else {
throw new XMLStreamException("Unknown tag: "
+ xsr.getLocalName());
}
}
if (server != null && active != null && game != null) {
// Now units are all present, set active unit.
FreeColGameObject fcgo = game.getFreeColGameObjectSafely(active);
if (fcgo instanceof Unit) {
server.setActiveUnit((Unit) fcgo);
}
}
return game;
} catch (Exception e) {
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
logger.warning(sw.toString());
throw new IOException(e.toString());
} finally {
if (xs != null) xs.close();
}
}
/**
* Loads a game.
*
* @param fis The file where the game data is located.
* @param specification a <code>Specification</code> value
* @return The new game.
* @exception FreeColException if the savegame contains incompatible data.
* @exception IOException if there is problem reading a stream.
*/
public Game loadGame(final FreeColSavegameFile fis,
Specification specification)
throws FreeColException, IOException {
ServerGame game = readGame(fis, specification, this);
gameState = GameState.IN_GAME;
integrity = game.checkIntegrity();
int savegameVersion = getSavegameVersion(fis);
// @compat 0.10.x
if (savegameVersion < 12) {
for (Player p : game.getPlayers()) {
if(!p.isIndian() && p.getEurope() != null) {
p.initializeHighSeas();
for (Unit u : p.getEurope().getUnitList()) {
// move units to high seas use setLocation()
// so that units are removed from Europe, and
// appear in correct panes in the EuropePanel
// do not set the UnitState, as this clears
// workLeft
if (u.getState() == Unit.UnitState.TO_EUROPE) {
logger.info("Found unit on way to europe: "
+ u.toString());
u.setLocation(p.getHighSeas());
u.setDestination(p.getEurope());
} else if (u.getState() == Unit.UnitState.TO_AMERICA) {
logger.info("Found unit on way to new world: "
+ u.toString());
u.setLocation(p.getHighSeas());
u.setDestination(getGame().getMap());
}
}
}
}
for (Tile tile : game.getMap().getAllTiles()) {
TerrainGenerator.encodeStyle(tile);
}
}
// end compatibility code
// @compat 0.9.x, 0.10.x
fixGameOptions();
// end compatibility code
// @compat 0.9.x
if (savegameVersion < 11) {
for (Tile t : game.getMap().getAllTiles()) t.fixup09x();
}
// end compatibility code
// AI initialization.
AIMain aiMain = getAIMain();
if (!aiMain.checkIntegrity()) {
aiMain = new AIMain(this);
logger.info("Replacing AIMain.");
}
game.setFreeColGameObjectListener(aiMain);
Collections.sort(game.getPlayers(), Player.playerComparator);
for (Player player : game.getPlayers()) {
if (player.isAI()) {
ServerPlayer serverPlayer = (ServerPlayer) player;
DummyConnection theConnection
= new DummyConnection("Server-Server-" + player.getName(),
getInGameInputHandler());
DummyConnection aiConnection
= new DummyConnection("Server-AI-" + player.getName(),
new AIInGameInputHandler(this, serverPlayer, aiMain));
aiConnection.setOutgoingMessageHandler(theConnection);
theConnection.setOutgoingMessageHandler(aiConnection);
getServer().addDummyConnection(theConnection);
serverPlayer.setConnection(theConnection);
serverPlayer.setConnected(true);
}
}
return game;
}
/**
* Gets the save game version from a saved game.
*
* @param fis The saved game.
* @return The saved game version.
* @exception FreeColException if the version is incompatible.
*/
private static int getSavegameVersion(final FreeColSavegameFile fis)
throws FreeColException {
XMLStream xs = null;
try {
xs = createXMLStreamReader(fis);
final XMLStreamReader xsr = xs.getXMLStreamReader();
xsr.nextTag();
final String version = xsr.getAttributeValue(null, "version");
int savegameVersion = 0;
try {
savegameVersion = Integer.parseInt(version);
} catch (Exception e) {
throw new FreeColException("incompatibleVersions");
}
if (savegameVersion < MINIMUM_SAVEGAME_VERSION) {
throw new FreeColException("incompatibleVersions");
}
return savegameVersion;
} catch (Exception e) {
throw new FreeColException(e.toString());
} finally {
if (xs != null) xs.close();
}
}
// @compat 0.9.x, 0.10.x
private void fixGameOptions() {
// Add a default value for options new to each version
// that are not part of the difficulty settings.
// Annotate with save format version where introduced
Specification spec = game.getSpecification();
// @compat 0.9.x
// Introduced: SAVEGAME_VERSION == 11
addIntegerOption("model.option.monarchSupport", "", 2);
// Introduced: SAVEGAME_VERSION == 11
addStringOption("model.option.buildOnNativeLand", "",
"model.option.buildOnNativeLand.never");
// Introduced: SAVEGAME_VERSION == 11
if (!spec.hasOption("model.option.amphibiousMoves")) {
addBooleanOption("model.option.amphibiousMoves",
"gameOptions.map", false);
spec.addModifier(new Modifier("model.modifier.amphibiousAttack",
Specification.AMPHIBIOUS_ATTACK_PENALTY_SOURCE,
-75.0f,
Modifier.Type.PERCENTAGE));
}
// Introduced: SAVEGAME_VERSION == 11
addBooleanOption("model.option.settlementActionsContactChief",
"gameOptions.map", false);
// @compat 0.10.x
// Introduced: SAVEGAME_VERSION == 12
addBooleanOption("model.option.enhancedMissionaries",
"gameOptions.map", false);
// Introduced: SAVEGAME_VERSION == 12
addBooleanOption("model.option.continueFoundingFatherRecruitment",
"gameOptions.map", false);
// Introduced: SAVEGAME_VERSION == 12
addIntegerOption("model.option.settlementLimitModifier",
"gameOptions.map", 0);
// Introduced: SAVEGAME_VERSION == 12
addIntegerOption("model.option.startingPositions",
"gameOptions.map", 0);
// Introduced: SAVEGAME_VERSION == 12
addBooleanOption(GameOptions.TELEPORT_REF,
"gameOptions.map", false);
// Introduced: SAVEGAME_VERSION == 12
addIntegerOption(GameOptions.SHIP_TRADE_PENALTY,
"gameOptions.map", -30);
if (spec.getModifiers("model.modifier.shipTradePenalty") == null) {
spec.addModifier(new Modifier("model.modifier.shipTradePenalty",
Specification.SHIP_TRADE_PENALTY_SOURCE,
-30.0f, Modifier.Type.PERCENTAGE));
}
}
private void addBooleanOption(String id, String gr, boolean defaultValue) {
Specification spec = game.getSpecification();
if (!spec.hasOption(id)) {
BooleanOption op = new BooleanOption(id);
op.setGroup(gr);
op.setValue(defaultValue);
spec.addAbstractOption(op);
if ("".equals(gr)) {
for (OptionGroup level : spec.getDifficultyLevels()) {
level.add(op);
}
} else {
spec.getOptionGroup(gr).add(op);
}
}
}
private void addIntegerOption(String id, String gr, int defaultValue) {
Specification spec = game.getSpecification();
if (!spec.hasOption(id)) {
IntegerOption op = new IntegerOption(id);
op.setGroup(gr);
op.setValue(defaultValue);
spec.addAbstractOption(op);
if ("".equals(gr)) {
for (OptionGroup level : spec.getDifficultyLevels()) {
level.add(op);
}
} else {
spec.getOptionGroup(gr).add(op);
}
}
}
private void addStringOption(String id, String gr, String defaultValue) {
Specification spec = game.getSpecification();
if (!spec.hasOption(id)) {
StringOption op = new StringOption(id);
op.setGroup(gr);
op.setValue(defaultValue);
spec.addAbstractOption(op);
if ("".equals(gr)) {
for (OptionGroup level : spec.getDifficultyLevels()) {
level.add(op);
}
} else {
spec.getOptionGroup(gr).add(op);
}
}
}
// end compatibility code
/**
* At savegame version 11 (new in 0.10.0) several classes were
* added to serverObjects. Evil hack here to scan the game for
* objects in pre-v11 games that should now be in serverObjects,
* and put them in.
*
* @param serverStrings A list of server object {type, id} pairs to add to.
* @param fis The savegame to scan.
* @compat 0.10.x
*/
private static void v11FixServerObjects(List<String> serverStrings,
final FreeColSavegameFile fis) {
XMLStream xs = null;
try {
xs = createXMLStreamReader(fis);
final XMLStreamReader xsr = xs.getXMLStreamReader();
xsr.nextTag();
final String owner = xsr.getAttributeValue(null, "owner");
while (xsr.nextTag() != XMLStreamConstants.END_ELEMENT) {
if (xsr.getLocalName().equals(Game.getXMLElementTagName())) {
Game game = new ServerGame(null, xsr,
new ArrayList<String>(serverStrings),
new FreeColTcFile("freecol").getSpecification());
Iterator<FreeColGameObject> objs
= game.getFreeColGameObjectIterator();
while (objs.hasNext()) {
FreeColGameObject fcgo = objs.next();
if (serverStrings.contains(fcgo.getId())) {
continue;
} else if (fcgo instanceof Europe) {
serverStrings.add("serverEurope");
} else if (fcgo instanceof Colony) {
serverStrings.add("serverColony");
} else if (fcgo instanceof Building) {
serverStrings.add("serverBuilding");
} else if (fcgo instanceof ColonyTile) {
serverStrings.add("serverColonyTile");
} else if (fcgo instanceof IndianSettlement) {
serverStrings.add("serverIndianSettlement");
} else if (fcgo instanceof Unit) {
serverStrings.add("serverUnit");
} else {
continue;
}
serverStrings.add(fcgo.getId());
}
break;
}
while (xsr.nextTag() != XMLStreamConstants.END_ELEMENT) {
xsr.nextTag();
}
}
} catch (Exception e) {
;
} finally {
if (xs != null) xs.close();
}
}
/**
* Removes automatically created save games.
* Call this function to delete the automatically created save games from
* a previous game.
*/
public static void removeAutosaves(final String prefix) {
for (File autosaveFile : FreeCol.getAutosaveDirectory().listFiles()) {
if (autosaveFile.getName().startsWith(prefix)) {
autosaveFile.delete();
}
}
}
/**
* Makes the entire map visible for all players. Used only when debugging
* (will be removed).
*/
public void revealMapForAllPlayers() {
Iterator<Player> playerIterator = getGame().getPlayerIterator();
while (playerIterator.hasNext()) {
ServerPlayer player = (ServerPlayer) playerIterator.next();
player.revealMap();
}
playerIterator = getGame().getPlayerIterator();
while (playerIterator.hasNext()) {
ServerPlayer player = (ServerPlayer) playerIterator.next();
Element reconnect = DOMMessage.createNewRootElement("reconnect");
try {
player.getConnection().send(reconnect);
} catch (IOException ex) {
logger.warning("Could not send reconnect message!");
}
}
}
/**
* Gets a <code>Player</code> specified by a connection.
*
* @param connection The connection to use while searching for a
* <code>ServerPlayer</code>.
* @return The player.
*/
public ServerPlayer getPlayer(Connection connection) {
Iterator<Player> playerIterator = getGame().getPlayerIterator();
while (playerIterator.hasNext()) {
ServerPlayer player = (ServerPlayer) playerIterator.next();
if (player.getConnection() == connection) {
return player;
}
}
return null;
}
/**
* Get a unit by ID, validating the ID as much as possible. Designed for
* message unpacking where the ID should not be trusted.
*
* @param unitId The ID of the unit to be found.
* @param serverPlayer The <code>ServerPlayer</code> to whom the unit must belong.
*
* @return The unit corresponding to the unitId argument.
* @throws IllegalStateException on failure to validate the unitId
* in any way.
* In the worst case this may be indicative of a malign client.
*/
public Unit getUnitSafely(String unitId, ServerPlayer serverPlayer)
throws IllegalStateException {
Game game = serverPlayer.getGame();
FreeColGameObject obj;
Unit unit;
if (unitId == null || unitId.length() == 0) {
throw new IllegalStateException("ID must not be empty.");
}
obj = game.getFreeColGameObjectSafely(unitId);
if (obj == null) {
throw new IllegalStateException("Not an object: " + unitId);
} else if (!(obj instanceof Unit)) {
throw new IllegalStateException("Unit expected, "
+ " got " + obj.getClass()
+ ": " + unitId);
}
unit = (Unit) obj;
if (unit.getOwner() != serverPlayer) {
throw new IllegalStateException("Not the owner of unit: " + unitId);
}
return unit;
}
/**
* Get a settlement by ID, validating the ID as much as possible.
* Designed for message unpacking where the ID should not be trusted.
*
* @param settlementId The ID of the <code>Settlement</code> to be found.
* @param unit A <code>Unit</code> which must be adjacent
* to the <code>Settlement</code>.
*
* @return The settlement corresponding to the settlementId argument.
* @throws IllegalStateException on failure to validate the settlementId
* in any way.
* In the worst case this may be indicative of a malign client.
*/
public Settlement getAdjacentSettlementSafely(String settlementId, Unit unit)
throws IllegalStateException {
Game game = unit.getOwner().getGame();
Settlement settlement;
if (settlementId == null || settlementId.length() == 0) {
throw new IllegalStateException("ID must not be empty.");
} else if (!(game.getFreeColGameObject(settlementId) instanceof Settlement)) {
throw new IllegalStateException("Not a settlement ID: " + settlementId);
}
settlement = (Settlement) game.getFreeColGameObject(settlementId);
if (settlement.getTile() == null) {
throw new IllegalStateException("Settlement is not on the map: "
+ settlementId);
}
if (unit.getTile() == null) {
throw new IllegalStateException("Unit is not on the map: "
+ unit.getId());
}
if (unit.getTile().getDistanceTo(settlement.getTile()) > 1) {
throw new IllegalStateException("Unit " + unit.getId()
+ " is not adjacent to settlement: " + settlementId);
}
if (unit.getOwner() == settlement.getOwner()) {
throw new IllegalStateException("Unit: " + unit.getId()
+ " and settlement: " + settlementId
+ " are both owned by player: "
+ unit.getOwner().getId());
}
return settlement;
}
/**
* Get an adjacent Indian settlement by ID, validating as much as possible,
* including checking whether the nation involved has been contacted.
* Designed for message unpacking where the ID should not be trusted.
*
* @param settlementId The ID of the <code>Settlement</code> to be found.
* @param unit A <code>Unit</code> which must be adjacent
* to the <code>Settlement</code>.
*
* @return The settlement corresponding to the settlementId argument.
* @throws IllegalStateException on failure to validate the settlementId
* in any way.
* In the worst case this may be indicative of a malign client.
*/
public IndianSettlement getAdjacentIndianSettlementSafely(String settlementId, Unit unit)
throws IllegalStateException {
Settlement settlement = getAdjacentSettlementSafely(settlementId, unit);
if (!(settlement instanceof IndianSettlement)) {
throw new IllegalStateException("Not an indianSettlement: " + settlementId);
}
if (!unit.getOwner().hasContacted(settlement.getOwner())) {
throw new IllegalStateException("Player has not established contact with the "
+ settlement.getOwner().getNation());
}
return (IndianSettlement) settlement;
}
/**
* Adds a new AIPlayer to the Game.
*
* @param nation a <code>Nation</code> value
* @return a <code>ServerPlayer</code> value
*/
public ServerPlayer addAIPlayer(Nation nation) {
String name = nation.getRulerNameKey();
DummyConnection theConnection =
new DummyConnection("Server connection - " + name, getInGameInputHandler());
ServerPlayer aiPlayer
= new ServerPlayer(getGame(), name, false, nation,
null, theConnection);
aiPlayer.setAI(true);
DummyConnection aiConnection
= new DummyConnection("AI connection - " + name,
new AIInGameInputHandler(this, aiPlayer, getAIMain()));
aiConnection.setOutgoingMessageHandler(theConnection);
theConnection.setOutgoingMessageHandler(aiConnection);
getServer().addDummyConnection(theConnection);
getGame().addPlayer(aiPlayer);
// Send message to all players except to the new player:
// TODO: null-destination-player is unnecessarily generous visibility
Element addNewPlayer = DOMMessage.createNewRootElement("addPlayer");
addNewPlayer.appendChild(aiPlayer.toXMLElement(null, addNewPlayer.getOwnerDocument()));
getServer().sendToAll(addNewPlayer, theConnection);
return aiPlayer;
}
/**
* Gets the AI player corresponding to a given player.
*
* @param player The <code>Player</code> to look up.
* @return The corresponding AI player, or null if not found.
*/
public AIPlayer getAIPlayer(Player player) {
return getAIMain().getAIPlayer(player);
}
/**
* Get the <code>HighScores</code> value.
*
* @return a <code>List<HighScore></code> value
*/
public List<HighScore> getHighScores() {
if (highScores == null) {
try {
loadHighScores();
} catch (Exception e) {
logger.warning(e.toString());
highScores = new ArrayList<HighScore>();
}
}
return highScores;
}
/**
* Adds a new high score for player and returns <code>true</code>
* if possible.
*
* @param player a <code>Player</code> value
* @return a <code>boolean</code> value
*/
public boolean newHighScore(Player player) {
getHighScores();
if (!highScores.isEmpty() && player.getScore() <= highScores.get(highScores.size() - 1).getScore()) {
return false;
} else {
highScores.add(new HighScore(player, new Date()));
Collections.sort(highScores, highScoreComparator);
if (highScores.size() == NUMBER_OF_HIGH_SCORES) {
highScores.remove(NUMBER_OF_HIGH_SCORES - 1);
}
return true;
}
}
/**
* Saves high scores.
*
* @throws IOException If a problem was encountered while trying to open,
* write or close the file.
*/
public void saveHighScores() throws IOException {
if (highScores == null || highScores.isEmpty()) {
return;
}
Collections.sort(highScores, highScoreComparator);
XMLOutputFactory xof = XMLOutputFactory.newInstance();
FileOutputStream fos = null;
try {
XMLStreamWriter xsw;
fos = new FileOutputStream(new File(FreeCol.getDataDirectory(), HIGH_SCORE_FILE));
xsw = xof.createXMLStreamWriter(fos, "UTF-8");
xsw.writeStartDocument("UTF-8", "1.0");
xsw.writeStartElement("highScores");
int count = 0;
for (HighScore score : highScores) {
score.toXML(xsw);
count++;
if (count == NUMBER_OF_HIGH_SCORES) {
break;
}
}
xsw.writeEndElement();
xsw.writeEndDocument();
xsw.flush();
xsw.close();
} catch (XMLStreamException e) {
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
logger.warning(sw.toString());
throw new IOException("XMLStreamException.");
} catch (Exception e) {
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
logger.warning(sw.toString());
throw new IOException(e.toString());
} finally {
try {
if (fos != null) {
fos.close();
}
} catch (IOException e) {
// do nothing
}
}
}
/**
* Loads high scores.
*
* @throws IOException If a problem was encountered while trying to open,
* read or close the file.
* @exception IOException if thrown while loading the game or if a
* <code>XMLStreamException</code> have been thrown by the
* parser.
* @exception FreeColException if the savegame contains incompatible data.
*/
public void loadHighScores() throws IOException, FreeColException {
highScores = new ArrayList<HighScore>();
File hsf = new File(FreeCol.getDataDirectory(), HIGH_SCORE_FILE);
if (!hsf.exists()) return;
XMLInputFactory xif = XMLInputFactory.newInstance();
FileInputStream fis = null;
try {
fis = new FileInputStream(hsf);
XMLStreamReader xsr = xif.createXMLStreamReader(fis, "UTF-8");
xsr.nextTag();
while (xsr.nextTag() != XMLStreamConstants.END_ELEMENT) {
if (xsr.getLocalName().equals("highScore")) {
highScores.add(new HighScore(xsr));
}
}
xsr.close();
Collections.sort(highScores, highScoreComparator);
} catch (XMLStreamException e) {
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
logger.warning(sw.toString());
throw new IOException("XMLStreamException.");
} catch (Exception e) {
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
logger.warning(sw.toString());
throw new IOException(e.toString());
} finally {
if (fis != null) {
fis.close();
}
}
}
public void shutdown() {
server.shutdown();
}
/**
* Get the internal state of a random number generator as a
* string. It would have been more convenient to simply return
* the current seed, but unfortunately it is private.
*
* @param random The <code>Random</code> to use.
* @return A <code>String</code> encapsulating the object state.
*/
public static synchronized String getRandomState(Random random) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(random);
oos.flush();
} catch (IOException e) {
throw new IllegalStateException("IO exception in memory!?", e);
}
byte[] bytes = bos.toByteArray();
StringBuffer sb = new StringBuffer(bytes.length * 2);
for (byte b : bytes) {
sb.append(HEX_DIGITS.charAt((b >> 4) & 0x0F));
sb.append(HEX_DIGITS.charAt(b & 0x0F));
}
return sb.toString();
}
/**
* Restore a previously saved state.
*
* @param state The saved state (@see #getRandomState()).
* @return The restored <code>Random</code>.
* @throws IOException if unable to restore state.
*/
public static synchronized Random restoreRandomState(String state)
throws IOException {
byte[] bytes = new byte[state.length() / 2];
int pos = 0;
for (int i = 0; i < bytes.length; i++) {
bytes[i] = (byte) HEX_DIGITS.indexOf(state.charAt(pos++));
bytes[i] <<= 4;
bytes[i] |= (byte) HEX_DIGITS.indexOf(state.charAt(pos++));
}
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bis);
try {
return (Random) ois.readObject();
} catch (ClassNotFoundException e) {
throw new IOException("Failed to restore ServerRandom!");
}
}
}