/*
* $Id$
*
* Copyright (c) 2000-2003 by Rodney Kinney
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License (LGPL) as published by the Free Software Foundation.
*
* This library 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, copies are available
* at http://www.opensource.org.
*/
package VASSAL.build.module;
import java.awt.Cursor;
import java.awt.event.ActionEvent;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import org.jdesktop.swingworker.SwingWorker;
import org.slf4j.LoggerFactory;
import VASSAL.Info;
import VASSAL.build.GameModule;
import VASSAL.build.module.map.PieceCollection;
import VASSAL.build.module.metadata.AbstractMetaData;
import VASSAL.build.module.metadata.MetaDataFactory;
import VASSAL.build.module.metadata.SaveMetaData;
import VASSAL.command.AddPiece;
import VASSAL.command.AlertCommand;
import VASSAL.command.Command;
import VASSAL.command.CommandEncoder;
import VASSAL.command.CommandFilter;
import VASSAL.command.ConditionalCommand;
import VASSAL.command.Logger;
import VASSAL.command.NullCommand;
import VASSAL.configure.DirectoryConfigurer;
import VASSAL.counters.GamePiece;
import VASSAL.i18n.Resources;
import VASSAL.launch.Launcher;
import VASSAL.tools.ErrorDialog;
import VASSAL.tools.ReadErrorDialog;
import VASSAL.tools.ThrowableUtils;
import VASSAL.tools.WarningDialog;
import VASSAL.tools.WriteErrorDialog;
import VASSAL.tools.filechooser.FileChooser;
import VASSAL.tools.filechooser.LogAndSaveFileFilter;
import VASSAL.tools.io.DeobfuscatingInputStream;
import VASSAL.tools.io.FastByteArrayOutputStream;
import VASSAL.tools.io.FileArchive;
import VASSAL.tools.io.IOUtils;
import VASSAL.tools.io.ObfuscatingOutputStream;
import VASSAL.tools.io.ZipArchive;
import VASSAL.tools.menu.MenuManager;
import VASSAL.tools.swing.Dialogs;
/**
* The GameState represents the state of the game currently being played.
* Only one game can be open at once.
* @see GameModule#getGameState
*/
public class GameState implements CommandEncoder {
private static final org.slf4j.Logger log =
LoggerFactory.getLogger(GameState.class);
protected Map<String,GamePiece> pieces = new HashMap<String,GamePiece>();
protected List<GameComponent> gameComponents = new ArrayList<GameComponent>();
protected List<GameSetupStep> setupSteps = new ArrayList<GameSetupStep>();
protected Action loadGame, saveGame, newGame, closeGame;
protected String lastSave;
protected DirectoryConfigurer savedGameDirectoryPreference;
protected String loadComments;
public GameState() {}
/**
* Expects to be added to a GameModule. Adds <code>New</code>,
* <code>Load</code>, <code>Close</code>, and <code>Save</code>
* entries to the <code>File</code> menu of the controls window
*/
public void addTo(GameModule mod) {
loadGame = new AbstractAction(Resources.getString("GameState.load_game")) {
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent e) {
loadGame();
}
};
// FIMXE: setting nmemonic from first letter could cause collisions in
// some languages
loadGame.putValue(Action.MNEMONIC_KEY, (int)Resources.getString("GameState.load_game.shortcut").charAt(0));
saveGame = new AbstractAction(Resources.getString("GameState.save_game")) {
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent e) {
saveGame();
}
};
// FIMXE: setting nmemonic from first letter could cause collisions in
// some languages
saveGame.putValue(Action.MNEMONIC_KEY, (int)Resources.getString("GameState.save_game.shortcut").charAt(0));
newGame = new AbstractAction(Resources.getString("GameState.new_game")) {
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent e) {
setup(false);
setup(true);
}
};
// FIMXE: setting nmemonic from first letter could cause collisions in
// some languages
newGame.putValue(Action.MNEMONIC_KEY, (int)Resources.getString("GameState.new_game.shortcut").charAt(0));
closeGame = new AbstractAction(
Resources.getString("GameState.close_game")) {
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent e) {
setup(false);
}
};
// FIMXE: setting nmemonic from first letter could cause collisions in
// some languages
closeGame.putValue(Action.MNEMONIC_KEY, (int)Resources.getString("GameState.close_game.shortcut").charAt(0));
final MenuManager mm = MenuManager.getInstance();
mm.addAction("GameState.new_game", newGame);
mm.addAction("GameState.load_game", loadGame);
mm.addAction("GameState.save_game", saveGame);
mm.addAction("GameState.close_game", closeGame);
saveGame.setEnabled(gameStarting);
closeGame.setEnabled(gameStarting);
}
/**
* @return true if the game state is different from when it was last saved
*/
public boolean isModified() {
String s = saveString();
return s != null && !s.equals(lastSave);
}
/**
* Add a {@link GameComponent} to the list of objects that will
* be notified when a game is started/ended
*/
public void addGameComponent(GameComponent theComponent) {
gameComponents.add(theComponent);
}
/**
* Remove a {@link GameComponent} from the list of objects that will
* be notified when a game is started/ended
*/
public void removeGameComponent(GameComponent theComponent) {
gameComponents.remove(theComponent);
}
/**
* @return an enumeration of all {@link GameComponent} objects
* that have been added to this GameState
* @deprecated Use {@link #getGameComponents()} instead.
*/
@Deprecated
public Enumeration<GameComponent> getGameComponentsEnum() {
return Collections.enumeration(gameComponents);
}
/**
* @return a Collection of all {@link GameComponent} objects
* that have been added to this GameState
*/
public Collection<GameComponent> getGameComponents() {
return Collections.unmodifiableCollection(gameComponents);
}
/** Add a {@link GameSetupStep} */
public void addGameSetupStep(GameSetupStep step) {
setupSteps.add(step);
}
/** Remove a {@link GameSetupStep} */
public void removeGameSetupStep(GameSetupStep step) {
setupSteps.remove(step);
}
/**
* @return an iterator of all {@link GameSetupStep}s that are not
* yet finished
*/
public Iterator<GameSetupStep> getUnfinishedSetupSteps() {
ArrayList<GameSetupStep> l = new ArrayList<GameSetupStep>();
for (GameSetupStep step : setupSteps) {
if (!step.isFinished()) {
l.add(step);
}
}
return l.iterator();
}
/* Using an instance variable allows us to shut down in the
middle of a startup. */
private boolean gameStarting = false;
private boolean gameStarted = false;
//
// FIXME: This will become unnecessary when we do model-view separation.
//
private volatile boolean gameUpdating = false;
/**
* Start a game for updating (via editor).
* <em>NOTE: This method is not for use in custom code.</em>
*/
public void setup(boolean gameStarting, boolean gameUpdating) {
this.gameUpdating = gameUpdating;
setup(gameStarting);
}
/**
* Indicated game update is completed and game is saved.
*/
public void updateDone() {
this.gameUpdating = false;
}
public boolean isUpdating() {
return this.gameUpdating;
}
//
// END FIXME
//
/**
* Start/end a game. Prompt to save if the game state has been
* modified since last save. Invoke {@link GameComponent#setup}
* on all registered {@link GameComponent} objects.
*/
public void setup(boolean gameStarting) {
if (!gameStarting && gameStarted && isModified()) {
switch (JOptionPane.showConfirmDialog(
GameModule.getGameModule().getFrame(),
Resources.getString("GameState.save_game_query"), //$NON-NLS-1$
Resources.getString("GameState.game_modified"), //$NON-NLS-1$
JOptionPane.YES_NO_CANCEL_OPTION)) {
case JOptionPane.YES_OPTION:
saveGame();
break;
case JOptionPane.CANCEL_OPTION:
case JOptionPane.CLOSED_OPTION:
return;
}
}
this.gameStarting = gameStarting;
if (!gameStarting) {
pieces.clear();
}
newGame.setEnabled(!gameStarting);
saveGame.setEnabled(gameStarting);
closeGame.setEnabled(gameStarting);
if (gameStarting) {
loadGame.putValue(Action.NAME,
Resources.getString("GameState.load_continuation"));
GameModule.getGameModule().getWizardSupport().showGameSetupWizard();
}
else {
loadGame.putValue(Action.NAME,
Resources.getString("GameState.load_game"));
GameModule.getGameModule().appendToTitle(null);
}
gameStarted &= this.gameStarting;
for (GameComponent gc : gameComponents) {
gc.setup(this.gameStarting);
}
gameStarted |= this.gameStarting;
lastSave = gameStarting ? saveString() : null;
}
/** Return true if a game is currently in progress */
public boolean isGameStarted() {
return gameStarted;
}
/**
* Read the game from a savefile. The contents of the file is
* sent to {@link GameModule#decode} and translated into a
* {@link Command}, which is then executed. The command read from the
* file should be that returned by {@link #getRestoreCommand}.
*/
public void loadGame() {
final GameModule g = GameModule.getGameModule();
loadComments = "";
final FileChooser fc = g.getFileChooser();
fc.addChoosableFileFilter(new LogAndSaveFileFilter());
if (fc.showOpenDialog() != FileChooser.APPROVE_OPTION) return;
final File f = fc.getSelectedFile();
try {
if (!f.exists()) throw new FileNotFoundException(
"Unable to locate " + f.getPath());
// Check the Save game for validity
final AbstractMetaData metaData = MetaDataFactory.buildMetaData(f);
if (metaData == null || ! (metaData instanceof SaveMetaData)) {
WarningDialog.show("GameState.invalid_save_file", f.getPath());
return;
}
// Check it belongs to this module and matches the version if is a
// post 3.0 save file
final SaveMetaData saveData = (SaveMetaData) metaData;
String saveModuleVersion = "?";
if (saveData.getModuleData() != null) {
loadComments = saveData.getLocalizedDescription();
final String saveModuleName = saveData.getModuleName();
saveModuleVersion = saveData.getModuleVersion();
final String moduleName = g.getGameName();
final String moduleVersion = g.getGameVersion();
String message = null;
if (!saveModuleName.equals(moduleName)) {
message = Resources.getString(
"GameState.load_module_mismatch",
f.getName(), saveModuleName, moduleName
);
}
else if (!saveModuleVersion.equals(moduleVersion)) {
message = Resources.getString(
"GameState.load_version_mismatch",
f.getName(), saveModuleVersion, moduleVersion
);
}
if (message != null) {
if (JOptionPane.showConfirmDialog(
null,
message,
Resources.getString("GameState.load_mismatch"),
JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE) != JOptionPane.YES_OPTION) {
g.warn(Resources.getString("GameState.cancel_load", f.getName()));
return;
}
}
}
log.info(
"Loading save game " + f.getPath() +
", created with module version " + saveModuleVersion
);
if (gameStarted) {
loadContinuation(f);
}
else {
loadGameInBackground(f);
}
}
catch (IOException e) {
ReadErrorDialog.error(e, f);
}
/*
String msg = Resources.getString("GameState.unable_to_load", f.getName()); //$NON-NLS-1$
if (e.getMessage() != null) {
msg += "\n" + e.getMessage(); //$NON-NLS-1$
}
JOptionPane.showMessageDialog(GameModule.getGameModule().getFrame(),
msg, Resources.getString("GameState.load_error"), JOptionPane.ERROR_MESSAGE); //$NON-NLS-1$
}
else {
// FIXME: give more specific error message
// FIXME: maybe deprecate warn()?
GameModule.getGameModule().warn(Resources.getString("GameState.unable_to_find", f.getPath())); //$NON-NLS-1$
}
*/
}
protected String saveString() {
return GameModule.getGameModule().encode(getRestoreCommand());
}
/** Prompts the user for a file into which to save the game */
public void saveGame() {
final GameModule g = GameModule.getGameModule();
g.warn(Resources.getString("GameState.saving_game")); //$NON-NLS-1$
final File saveFile = getSaveFile();
if (saveFile == null) {
g.warn(Resources.getString("GameState.save_canceled")); //$NON-NLS-1$
}
else {
if (saveFile.exists()) {
// warn user if overwriting a save from an old version
final AbstractMetaData md = MetaDataFactory.buildMetaData(saveFile);
if (md != null && md instanceof SaveMetaData) {
if (Info.hasOldFormat(md.getVassalVersion())) {
if (Dialogs.showConfirmDialog(
g.getFrame(),
Resources.getString("Warning.save_will_be_updated_title"),
Resources.getString("Warning.save_will_be_updated_heading"),
Resources.getString(
"Warning.save_will_be_updated_message",
saveFile.getPath(),
"3.2"
),
JOptionPane.WARNING_MESSAGE,
JOptionPane.OK_CANCEL_OPTION) == JOptionPane.CANCEL_OPTION)
{
return;
}
}
}
}
try {
saveGame(saveFile);
g.warn(Resources.getString("GameState.game_saved")); //$NON-NLS-1$
}
catch (IOException e) {
WriteErrorDialog.error(e, saveFile);
/*
Logger.log(err);
GameModule.getGameModule().warn(Resources.getString("GameState.save_failed")); //$NON-NLS-1$
*/
}
}
}
public void setModified(boolean modified) {
if (modified) {
lastSave = null;
}
else {
lastSave = saveString();
}
}
private File getSaveFile() {
final FileChooser fc = GameModule.getGameModule().getFileChooser();
fc.selectDotSavFile();
fc.addChoosableFileFilter(new LogAndSaveFileFilter());
if (fc.showSaveDialog() != FileChooser.APPROVE_OPTION) return null;
File file = fc.getSelectedFile();
if (file.getName().indexOf('.') == -1)
file = new File(file.getParent(), file.getName() + ".vsav");
return file;
}
/**
* Add a {@link GamePiece} to the current game.
* The GameState keeps track of all GamePieces in the system,
* regardless of which {@link Map} they belong to (if any)
*/
public void addPiece(GamePiece p) {
if (p.getId() == null) {
p.setId(getNewPieceId());
}
pieces.put(p.getId(), p);
}
/**
* @return the {@link GamePiece} in the current game with the given id
*/
public GamePiece getPieceForId(String id) {
return id == null ? null : pieces.get(id);
}
/**
* Remove a {@link GamePiece} from the current game
*/
public void removePiece(String id) {
if (id != null) {
pieces.remove(id);
}
}
/**
* @return a String identifier guaranteed to be different from the
* id of all GamePieces in the game
*
* @see GamePiece#getId
*/
public String getNewPieceId() {
long time = System.currentTimeMillis();
String id = Long.toString(time);
while (pieces.get(id) != null) {
id = Long.toString(++time);
}
return id;
}
public void loadContinuation(File f) throws IOException {
GameModule.getGameModule().warn(
Resources.getString("GameState.loading", f.getName())); //$NON-NLS-1$
Command c = decodeSavedGame(f);
CommandFilter filter = new CommandFilter() {
protected boolean accept(Command c) {
return c instanceof BasicLogger.LogCommand;
}
};
c = filter.apply(c);
if (c != null) {
c.execute();
}
String msg = Resources.getString("GameState.loaded", f.getName()); //$NON-NLS-1$
if (loadComments != null && loadComments.length() > 0) {
msg += ": " + loadComments;
}
GameModule.getGameModule().warn(msg);
}
/**
* @return an Enumeration of all {@link GamePiece}s in the game
* @deprecated Use {@link #getAllPieces()} instead.
*/
@Deprecated
public Enumeration<GamePiece> getPieces() {
return Collections.enumeration(pieces.values());
}
/** @return a Collection of all {@link GamePiece}s in the game */
public Collection<GamePiece> getAllPieces() {
return pieces.values();
}
public static class SetupCommand extends Command {
private boolean gameStarting;
public SetupCommand(boolean gameStarting) {
this.gameStarting = gameStarting;
}
public boolean isGameStarting() {
return gameStarting;
}
protected void executeCommand() {
GameModule.getGameModule().getGameState().setup(gameStarting);
}
protected Command myUndoCommand() {
return null;
}
}
public static final String SAVEFILE_ZIP_ENTRY = "savedGame"; //$NON-NLS-1$
/**
* Return a {@link Command} that, when executed, will restore the
* game to its current state. Invokes {@link GameComponent#getRestoreCommand}
* on each registered {@link GameComponent} */
public Command getRestoreCommand() {
if (!saveGame.isEnabled()) {
return null;
}
Command c = new SetupCommand(false);
c.append(checkVersionCommand());
c.append(getRestorePiecesCommand());
for (GameComponent gc : gameComponents) {
c.append(gc.getRestoreCommand());
}
c.append(new SetupCommand(true));
return c;
}
private Command checkVersionCommand() {
String runningVersion = GameModule.getGameModule().getAttributeValueString(GameModule.VASSAL_VERSION_RUNNING);
ConditionalCommand.Condition cond = new ConditionalCommand.Lt(GameModule.VASSAL_VERSION_RUNNING, runningVersion);
Command c = new ConditionalCommand(new ConditionalCommand.Condition[]{cond}, new AlertCommand(Resources.getString("GameState.version_mismatch", runningVersion))); //$NON-NLS-1$
String moduleName = GameModule.getGameModule().getAttributeValueString(GameModule.MODULE_NAME);
String moduleVersion = GameModule.getGameModule().getAttributeValueString(GameModule.MODULE_VERSION);
cond = new ConditionalCommand.Lt(GameModule.MODULE_VERSION, moduleVersion);
c.append(new ConditionalCommand(new ConditionalCommand.Condition[]{cond}, new AlertCommand(Resources.getString("GameState.version_mismatch2", moduleName, moduleVersion )))); //$NON-NLS-1$
return c;
}
/**
* A GameState recognizes instances of {@link SetupCommand}
*/
public String encode(Command c) {
if (c instanceof SetupCommand) {
return ((SetupCommand) c).isGameStarting() ? END_SAVE : BEGIN_SAVE;
}
else {
return null;
}
}
/**
* A GameState recognizes instances of {@link SetupCommand}
*/
public Command decode(String theCommand) {
if (BEGIN_SAVE.equals(theCommand)) {
return new SetupCommand(false);
}
else if (END_SAVE.equals(theCommand)) {
return new SetupCommand(true);
}
else {
return null;
}
}
public static final String BEGIN_SAVE = "begin_save"; //$NON-NLS-1$
public static final String END_SAVE = "end_save"; //$NON-NLS-1$
public void saveGame(File f) throws IOException {
// FIXME: Extremely inefficient! Write directly to ZipArchive OutputStream
final String save = saveString();
final FastByteArrayOutputStream ba = new FastByteArrayOutputStream();
OutputStream out = null;
try {
out = new ObfuscatingOutputStream(ba);
out.write(save.getBytes("UTF-8"));
out.close();
}
finally {
IOUtils.closeQuietly(out);
}
FileArchive archive = null;
try {
archive = new ZipArchive(f);
archive.add(SAVEFILE_ZIP_ENTRY, ba.toInputStream());
(new SaveMetaData()).save(archive);
archive.close();
}
finally {
IOUtils.closeQuietly(archive);
}
Launcher.getInstance().sendSaveCmd(f);
setModified(false);
}
public void loadGameInBackground(final File f) {
try {
loadGameInBackground(f.getName(),
new BufferedInputStream(new FileInputStream(f)));
}
catch (IOException e) {
ReadErrorDialog.error(e, f);
}
}
public void loadGameInBackground(final String shortName,
final InputStream in) {
GameModule.getGameModule().warn(
Resources.getString("GameState.loading", shortName)); //$NON-NLS-1$
final JFrame frame = GameModule.getGameModule().getFrame();
frame.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
new SwingWorker<Command,Void>() {
@Override
public Command doInBackground() throws Exception {
try {
return decodeSavedGame(in);
}
finally {
IOUtils.closeQuietly(in);
}
}
@Override
protected void done() {
try {
Command loadCommand = null;
String msg = null;
try {
loadCommand = get();
if (loadCommand != null) {
msg = Resources.getString("GameState.loaded", shortName); //$NON-NLS-1$
if (loadComments != null && loadComments.length() > 0) {
msg += ": " + loadComments;
}
}
else {
msg = Resources.getString("GameState.invalid_savefile", shortName); //$NON-NLS-1$
}
}
catch (InterruptedException e) {
ErrorDialog.bug(e);
}
// FIXME: review error message
catch (ExecutionException e) {
// FIXME: This is a temporary hack to catch OutOfMemoryErrors; there should
// be a better, more uniform and more permanent way of handling these, since
// an OOME is neither a VASSAL bug, a module bug, nor due to bad data.
final OutOfMemoryError oom =
ThrowableUtils.getAncestor(OutOfMemoryError.class, e);
if (oom != null) {
ErrorDialog.bug(e);
}
else {
log.error("", e);
}
msg = Resources.getString("GameState.error_loading", shortName);
}
if (loadCommand != null) {
loadCommand.execute();
}
GameModule.getGameModule().warn(msg);
Logger logger = GameModule.getGameModule().getLogger();
if (logger instanceof BasicLogger) {
((BasicLogger)logger).queryNewLogFile(true);
}
}
finally {
frame.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
}
}.execute();
}
/**
* @return a Command that, when executed, will add all pieces currently
* in the game. Used when saving a game.
*/
public Command getRestorePiecesCommand() {
// TODO remove stacks that were empty when the game was loaded and are still empty now
final List<GamePiece> pieceList = new ArrayList<GamePiece>(pieces.values());
Collections.sort(pieceList, new Comparator<GamePiece>() {
private final Map<GamePiece,Integer> indices = new HashMap<GamePiece,Integer>();
// Cache indices because indexOf() is linear;
// otherwise sorting would be quadratic.
private int indexOf(GamePiece p, VASSAL.build.module.Map m) {
Integer pi = indices.get(p);
if (pi == null) {
indices.put(p, pi = m.getPieceCollection().indexOf(p));
}
return pi;
}
public int compare(GamePiece a, GamePiece b) {
final VASSAL.build.module.Map amap = a.getMap(), bmap = b.getMap();
if (amap == null) {
return bmap == null ?
// order by id if neither piece is on a map
a.getId().compareTo(b.getId()) :
// nonnull map sorts before null map
-1;
}
else if (bmap == null) {
// null map sorts after nonnull map
return 1;
}
else if (amap == bmap) {
// same map, sort according to piece list
return indexOf(a, amap) - indexOf(b, bmap);
}
else {
// different maps, order by map
return amap.getId().compareTo(bmap.getId());
}
}
});
final Command c = new NullCommand();
for (GamePiece p : pieceList) {
c.append(new AddPiece(p));
}
return c;
}
/**
* Read a saved game and translate it into a Command. Executing the
* command will load the saved game.
*
* @param fileName
* @return
* @throws IOException
*/
public Command decodeSavedGame(File saveFile) throws IOException {
return decodeSavedGame(
new BufferedInputStream(new FileInputStream(saveFile)));
}
public Command decodeSavedGame(InputStream in) throws IOException {
ZipInputStream zipInput = null;
try {
zipInput = new ZipInputStream(in);
for (ZipEntry entry = zipInput.getNextEntry(); entry != null;
entry = zipInput.getNextEntry()) {
if (SAVEFILE_ZIP_ENTRY.equals(entry.getName())) {
InputStream din = null;
try {
din = new DeobfuscatingInputStream(zipInput);
// FIXME: toString() is very inefficient, make decode() use the stream directly
final Command c = GameModule.getGameModule().decode(
IOUtils.toString(din, "UTF-8"));
din.close();
return c;
}
finally {
IOUtils.closeQuietly(din);
}
}
}
zipInput.close();
}
finally {
IOUtils.closeQuietly(zipInput);
}
// FIXME: give more specific error message
throw new IOException("Invalid saveFile format");
}
public DirectoryConfigurer getSavedGameDirectoryPreference() {
if (savedGameDirectoryPreference == null) {
savedGameDirectoryPreference = new DirectoryConfigurer("savedGameDir", null);
GameModule.getGameModule().getPrefs().addOption(null,savedGameDirectoryPreference);
}
return savedGameDirectoryPreference;
}
}