package games.strategy.engine.framework;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.Iterator;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import javax.swing.JDialog;
import javax.swing.JOptionPane;
import games.strategy.debug.ClientLogger;
import games.strategy.engine.ClientContext;
import games.strategy.engine.ClientFileSystemHelper;
import games.strategy.engine.data.GameData;
import games.strategy.engine.delegate.IDelegate;
import games.strategy.engine.framework.headlessGameServer.HeadlessGameServer;
import games.strategy.triplea.UrlConstants;
import games.strategy.util.ThreadUtil;
import games.strategy.util.Version;
/**
* <p>
* Title: TripleA
* </p>
* <p>
* Description: Responsible for loading saved games, new games from xml, and saving games.
* </p>
*/
public class GameDataManager {
private static final String DELEGATE_START = "<DelegateStart>";
private static final String DELEGATE_DATA_NEXT = "<DelegateData>";
private static final String DELEGATE_LIST_END = "<EndDelegateList>";
public GameDataManager() {}
public GameData loadGame(final File savedGameFile) throws IOException {
try (
FileInputStream fileInputStream = new FileInputStream(savedGameFile);
InputStream input = new BufferedInputStream(fileInputStream)) {
String path;
try {
path = savedGameFile.getCanonicalPath();
} catch (final IOException e) {
path = savedGameFile.getPath();
}
return loadGame(input, path);
}
}
public GameData loadGame(final InputStream inputStream, final String savegamePath) throws IOException {
ObjectInputStream input = new ObjectInputStream(new GZIPInputStream(inputStream));
try {
final Version readVersion = (Version) input.readObject();
final boolean headless = HeadlessGameServer.headless();
if (!readVersion.equals(ClientContext.engineVersion().getVersion(), true)) {
// a hack for now, but a headless server should not try to open any savegame that is not its version
if (headless) {
final String message = "Incompatible game save, we are: " + ClientContext.engineVersion().getVersion()
+ " Trying to load game created with: " + readVersion;
HeadlessGameServer.sendChat(message);
System.out.println(message);
return null;
}
final String error = "<html>Incompatible engine versions, and no old engine found. We are: "
+ ClientContext.engineVersion().getVersion() + " . Trying to load game created with: " + readVersion
+ "<br>To download the latest version of TripleA, Please visit "
+ UrlConstants.LATEST_GAME_DOWNLOAD_WEBSITE + "</html>";
if (savegamePath == null) {
throw new IOException(error);
}
// so, what we do here is try to see if our installed copy of triplea includes older jars with it that are the
// same engine as was
// used for this savegame, and if so try to run it
try {
final String newClassPath = GameRunner.findOldJar(readVersion, true);
// ask user if we really want to do this?
final String messageString = "<html>This TripleA engine is version "
+ ClientContext.engineVersion().getVersion()
+ " and you are trying to open a savegame made with version " + readVersion.toString()
+ "<br>However, this TripleA cannot open any savegame made by any engine other than engines with the "
+ "same first three version numbers as it (x_x_x_x)."
+ "<br><br>TripleA now comes with older engines included with it, and has found the engine to run this "
+ "savegame. This is a new feature and is in 'beta' stage."
+ "<br>It will attempt to run a new instance of TripleA using the older engine jar file, and this "
+ "instance will only be able to play this savegame."
+ "<br><b>You may choose to either Close or Keep the current instance of TripleA!</b> (If hosting, you "
+ "must close it). Please report any bugs or issues."
+ "<br><br>Do you wish to continue?</html>";
final String yesClose = "Yes & Close Current";
final String yesOpen = "Yes & Do Not Close";
final String cancel = "Cancel";
final Object[] options = new Object[] {yesClose, yesOpen, cancel};
final JOptionPane pane = new JOptionPane(messageString, JOptionPane.PLAIN_MESSAGE,
JOptionPane.YES_NO_CANCEL_OPTION, null, options, yesClose);
final JDialog window = pane.createDialog(null, "Run old jar to open old Save Game?");
window.setVisible(true);
final Object buttonPressed = pane.getValue();
if (buttonPressed == null || buttonPressed.equals(cancel)) {
return null;
}
final boolean closeCurrentInstance = buttonPressed.equals(yesClose);
GameRunner.startGame(savegamePath, newClassPath, null);
if (closeCurrentInstance) {
ThreadUtil.sleep(1000);
System.exit(0);
}
} catch (final IOException e) {
if (ClientFileSystemHelper.areWeOldExtraJar()) {
throw new IOException("<html>Please run the default TripleA and try to open this game again. "
+ "<br>This TripleA engine is old and kept only for backwards compatibility and can only open "
+ "savegames created by engines with these first 3 version digits: "
+ ClientContext.engineVersion().getVersion().toStringFull("_", true) + "</html>");
} else {
throw new IOException(error);
}
}
return null;
} else if (!headless && readVersion.isGreaterThan(ClientContext.engineVersion().getVersion(), false)) {
// we can still load it because first 3 numbers of the version are the same, however this save was made by a
// newer engine, so prompt
// the user to upgrade
final String messageString =
"<html>Your TripleA engine is OUT OF DATE. This save was made by a newer version of TripleA."
+ "<br>However, because the first 3 version numbers are the same as your current version, we can "
+ "still open the savegame."
+ "<br><br>This TripleA engine is version "
+ ClientContext.engineVersion().getVersion().toStringFull("_")
+ " and you are trying to open a savegame made with version " + readVersion.toStringFull("_")
+ "<br><br>To download the latest version of TripleA, Please visit "
+ UrlConstants.LATEST_GAME_DOWNLOAD_WEBSITE
+ "<br><br>It is recommended that you upgrade to the latest version of TripleA before playing this "
+ "savegame."
+ "<br><br>Do you wish to continue and open this save with your current 'old' version?</html>";
final int answer =
JOptionPane.showConfirmDialog(null, messageString, "Open Newer Save Game?", JOptionPane.YES_NO_OPTION);
if (answer != JOptionPane.YES_OPTION) {
return null;
}
}
final GameData data = (GameData) input.readObject();
// TODO: expand this functionality (and keep it updated)
updateDataToBeCompatibleWithNewEngine(readVersion, data);
loadDelegates(input, data);
data.postDeSerialize();
return data;
} catch (final ClassNotFoundException cnfe) {
throw new IOException(cnfe.getMessage());
}
}
/**
* Use this to keep compatibility between savegames when it is easy to do so.
* When it is not easy to do so, just make sure to include the last release's .jar file in the "old" folder for
* triplea.
* FYI: Engine version numbers work like this with regards to savegames:
* Any changes to the first 3 digits means that the savegame is not compatible between different engines.
* While any change only to the 4th (last) digit means that the savegame must be compatible between different engines.
*/
private void updateDataToBeCompatibleWithNewEngine(final Version originalEngineVersion, final GameData data) {
// whenever this gets out of date, just comment out (but keep as an example, by commenting out)
/*
* example1:
* final Version v1610 = new Version(1, 6, 1, 0);
* final Version v1620 = new Version(1, 6, 2, 0);
* if (originalEngineVersion.equals(v1610, false)
* && ClientContext.engineVersion().getVersion().isGreaterThan(v1610, false)
* && ClientContext.engineVersion().getVersion().isLessThan(v1620, true))
* {
* // if original save was done under 1.6.1.0, and new engine is greater than 1.6.1.0 and less than 1.6.2.0
* try
* {
* if (TechAdvance.getTechAdvances(data).isEmpty())
* {
* System.out.println("Adding tech to be compatible with 1.6.1.x");
* TechAdvance.createDefaultTechAdvances(data);
* TechAbilityAttachment.setDefaultTechnologyAttachments(data);
* }
* } catch (final Exception e)
* {
* ClientLogger.logQuietly(e);
* }
* }
*/
}
private void loadDelegates(final ObjectInputStream input, final GameData data)
throws ClassNotFoundException, IOException {
for (Object endMarker = input.readObject(); !endMarker.equals(DELEGATE_LIST_END); endMarker = input.readObject()) {
final String name = (String) input.readObject();
final String displayName = (String) input.readObject();
final String className = (String) input.readObject();
IDelegate instance;
try {
instance = (IDelegate) Class.forName(className).newInstance();
instance.initialize(name, displayName);
data.getDelegateList().addDelegate(instance);
} catch (final Exception e) {
ClientLogger.logQuietly(e);
throw new IOException(e.getMessage());
}
final String next = (String) input.readObject();
if (next.equals(DELEGATE_DATA_NEXT)) {
instance.loadState((Serializable) input.readObject());
}
}
}
public void saveGame(final OutputStream sink, final GameData data) throws IOException {
saveGame(sink, data, true);
}
public void saveGame(final OutputStream sink, final GameData data, final boolean saveDelegateInfo)
throws IOException {
// write internally first in case of error
final ByteArrayOutputStream bytes = new ByteArrayOutputStream(25000);
final ObjectOutputStream outStream = new ObjectOutputStream(bytes);
outStream.writeObject(games.strategy.engine.ClientContext.engineVersion().getVersion());
data.acquireReadLock();
try {
outStream.writeObject(data);
if (saveDelegateInfo) {
writeDelegates(data, outStream);
} else {
outStream.writeObject(DELEGATE_LIST_END);
}
} finally {
data.releaseReadLock();
}
try (final GZIPOutputStream zippedOut = new GZIPOutputStream(sink)) {
// now write to file
zippedOut.write(bytes.toByteArray());
}
}
private void writeDelegates(final GameData data, final ObjectOutputStream out) throws IOException {
final Iterator<IDelegate> iter = data.getDelegateList().iterator();
while (iter.hasNext()) {
out.writeObject(DELEGATE_START);
final IDelegate delegate = iter.next();
// write out the delegate info
out.writeObject(delegate.getName());
out.writeObject(delegate.getDisplayName());
out.writeObject(delegate.getClass().getName());
out.writeObject(DELEGATE_DATA_NEXT);
out.writeObject(delegate.saveState());
}
// mark end of delegate section
out.writeObject(DELEGATE_LIST_END);
}
}