//
// @(#)PersistenceManager.java 4/2002
//
// Copyright 2002 Zachary DelProposto. All rights reserved.
// Use is subject to license terms.
//
//
// This program 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.
//
// This program 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 this program; if not, write to the Free Software
// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
// Or from http://www.gnu.org/
//
package dip.gui;
import dip.gui.dialog.*;
import dip.gui.dialog.newgame.*;
import dip.gui.dialog.prefs.*;
import dip.judge.gui.FlocImportDialog;
import dip.gui.report.ResultWriter;
import dip.world.InvalidWorldException;
import dip.world.World;
import dip.world.Phase;
import dip.world.TurnState;
import dip.world.variant.VariantManager;
import dip.world.variant.data.Variant;
import dip.misc.Help;
import dip.misc.Utils;
import dip.gui.swing.XJFileChooser;
import dip.misc.SimpleFileFilter;
import dip.misc.Log;
import dip.judge.parser.JudgeImport;
import dip.world.WorldFactory;
import dip.world.variant.data.SymbolPack;
import java.awt.Dimension;
import java.io.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.*;
import java.beans.*;
import java.util.*;
/**
* Manages saving / opening of game files, and creation of new games, and exiting of the program.
* <p>
* Ensures user can save any changes (if changes were made) before committing to an action
* that cannot be undone.
* <p>
* Also sets the main frame title.
*/
public class PersistenceManager
{
// i18n constants
private static final String CONFIRM_TEXT = "PM.dialog.confirm.location.text";
private static final String CONFIRM_TITLE = "PM.dialog.confirm.title";
private static final String CONFIRM_BUTTON_SAVE = "PM.dialog.confirm.save";
private static final String CONFIRM_BUTTON_DONTSAVE = "PM.dialog.confirm.dontsave";
private static final String CONFIRM_BUTTON_CANCEL = "PM.dialog.confirm.cancel";
private static final String CONFIRM_BUTTON_REWIND = "PM.dialog.confirm.rewind";
private static final String CONFIRM_BUTTON_LOAD = "PM.dialog.confirm.load";
private static final String CONFIRM_REWIND_TEXT = "PM.dialog.confirm.rewind.text";
private static final String CONFIRM_REWIND_TITLE = "PM.dialog.confirm.rewind.title";
private static final String CONFIRM_LOAD_TEXT = "PM.dialog.confirm.load.text";
private static final String CONFIRM_LOAD_TITLE = "PM.dialog.confirm.load.title";
private static final String UNSAVED_NAME = "PM.noname";
//private static final String OVERWRITE_TEXT = "PM.dialog.overwrite.text.location";
//private static final String OVERWRITE_TITLE = "PM.dialog.overwrite.title";
private static final String IMPORT_CHOOSER_TITLE = "PM.dialog.import.title";
private static final String MODIFIED_INDICATOR = "PM.indicator.modified";
private static final String SAVE_TO_TITLE = "PM.chooser.title.saveto";
private static final String EMPTY = "";
// internal constants
private final static String WINDOW_MODIFIED = "windowModified";
private final static long THREAD_WAIT = 7500L;
// instance variables
private ClientFrame clientFrame = null;
private boolean isChanged = false;
private File fileName = null;
private PropertyChangeListener modListener = null;
private final ThreadGroup persistTG;
/** Creates a new PersistenceManager object. */
public PersistenceManager(ClientFrame clientFrame)
{
this.clientFrame = clientFrame;
// create the persistance-manager threadgroup
persistTG = new ThreadGroup(Thread.currentThread().getThreadGroup(), "jdipPMGroup");
// by default, disable Save/Save As until we open/new something.
setSaveEnabled(false);
setTitle();
// enable modification event listener
modListener = new PropertyChangeListener()
{
public void propertyChange(PropertyChangeEvent evt)
{
if(!isChanged())
{
setChanged(true);
}
}// propertyChange()
};
clientFrame.addPropertyChangeListener(ClientFrame.EVT_MODIFIED_STATE, modListener);
}// PersistenceManager()
/** Cleanup */
public void close()
{
clientFrame.removePropertyChangeListener(modListener);
}// close()
/**
* Threads added to this ThreadGroup will be joined() at exit
* (with a pre-defined timeout) such that they will complete
* before jDip exits.
* <p>
* This ensures that certain IO operations (e.g., Exports)
* will not be aborted at exit.
*/
public ThreadGroup getPMThreadGroup()
{
return persistTG;
}// getPMThreadGroup()
/** If any change has occured singe the last time we saved. */
public boolean isChanged()
{
return isChanged;
}// isChanged()
/** Force an update of the game name / title bar */
public void updateTitle()
{
setTitle();
}// updateTitle()
/** Exit from the program, after confirmation */
public void exit()
{
if(confirmDialog())
{
GeneralPreferencePanel.saveWindowSettings(clientFrame);
// shutdown Batik renderer. This should stop the occasional
// IllegalComponentState exceptions when exiting during a
// render.
if(clientFrame.getMapPanel() != null)
{
clientFrame.getMapPanel().close();
}
clientFrame.setVisible(false);
// wait for any active threads in persistTG; if there are none,
final int activeCount = persistTG.activeCount();
Log.println("PM::exit(): threads pending: ", activeCount);
final Thread[] pendingThreads = new Thread[activeCount];
final int actualCount = persistTG.enumerate(pendingThreads);
Log.println("PM::exit(): actual threads pending: ", actualCount);
for(int i=0; i<pendingThreads.length; i++)
{
if(pendingThreads[i].isAlive())
{
try
{
Log.println("PM::exit(): waiting on ", pendingThreads[i].getName());
pendingThreads[i].join(THREAD_WAIT);
Log.println(" done.");
}
catch(Throwable t)
{
Log.println("PM::exit(): uncaught exception:");
Log.println(t);
}
}
}
clientFrame.dispose();
System.exit(0);
}
}// exit()
/** Opens a World from the given File, after confirmation */
public World open(File file)
{
World world = null;
if(confirmDialog())
{
try
{
world = readGameFile(file);
}
catch(Exception e)
{
ErrorDialog.displayFileIO(clientFrame, e, file.toString());
}
openWorld(world, file);
return world;
}
return world;
}// open()
/** Opens a world, displaying a FileChooser dialog.
* <br> Returns null if no file is chosen, or an error occurs.
*/
public World open()
{
if(confirmDialog())
{
// JFileChooser setup
XJFileChooser chooser = XJFileChooser.getXJFileChooser();
chooser.addFileFilter(SimpleFileFilter.SAVE_GAME_FILTER);
chooser.setFileFilter(SimpleFileFilter.SAVE_GAME_FILTER);
chooser.setCurrentDirectory( GeneralPreferencePanel.getDefaultGameDir() );
File file = chooser.displayOpen(clientFrame);
XJFileChooser.dispose();
// get file name
if(file != null)
{
World world = null;
try
{
world = readGameFile(file);
}
catch(Exception e)
{
ErrorDialog.displayFileIO(clientFrame, e, file.toString());
}
openWorld(world, file);
return world;
}
}
return null;
}// open()
/**
* Basic operations performed whenever we read in a World.
* if passed World is null, does nothing.
*/
private void openWorld(World world, File file)
{
if(world == null)
{
return;
}
fileName = file;
setChanged(false);
setSaveEnabled(true);
setTitle(world);
GeneralPreferencePanel.setRecentFileName(file);
clientFrame.getClientMenu().setSelected(ClientMenu.EDIT_EDIT_MODE, false);
clientFrame.getClientMenu().updateRecentFiles();
// this is for compatibility
//
if( !(world.getGameSetup() instanceof GUIGameSetup) )
{
// use a default game setup, if none exists
// note in log file.
Log.println("PM: no GuiGameSetup; creating a default.");
world.setGameSetup(new DefaultGUIGameSetup());
}
}// openWorld()
public World newNetworkGame()
{
try {
Variant variant = VariantManager.getVariant("standard", VariantManager.VERSION_NEWEST);
World world = WorldFactory.getInstance().createWorld(variant);
// set essential world data (variant name, map graphic to use)
World.VariantInfo variantInfo = world.getVariantInfo();
variantInfo.setVariantName( variant.getName() );
variantInfo.setVariantVersion( variant.getVersion() );
variantInfo.setMapName( variant.getDefaultMapGraphic().getName() );
// set GameSetup object
world.setGameSetup(new DefaultGUIGameSetup());
if (world != null) {
fileName = null;
setChanged(false);
setSaveEnabled(true);
clientFrame.getClientMenu().setSelected(ClientMenu.EDIT_EDIT_MODE, false);
clientFrame.getClientMenu().setViewNamesNone();
setTitle(world);
// set GameSetup object
world.setGameSetup(new NETGUIGameSetup());
return world;
}
return null;
} catch (InvalidWorldException ex) {
Logger.getLogger(PersistenceManager.class.getName()).log(Level.SEVERE, null, ex);
return null;
}
}
/**
* Creates a new game (Displays New Game dialog), after
* confirmation
*/
public World newGame()
{
if(confirmDialog())
{
World world = NewGameDialog.displayDialog(clientFrame);
if(world != null)
{
fileName = null;
setChanged(false);
setSaveEnabled(true);
clientFrame.getClientMenu().setSelected(ClientMenu.EDIT_EDIT_MODE, false);
clientFrame.getClientMenu().setViewNamesNone();
setTitle(world);
// set GameSetup object
world.setGameSetup(new DefaultGUIGameSetup());
return world;
}
}
return null;
}// newGame()
/**
* Creates a new game (Displays New Game dialog), after
* confirmation
*/
public World newF2FGame()
{
if(confirmDialog())
{
World world = NewGameDialog.displayDialog(
clientFrame,
Utils.getLocalString(NewGameDialog.TITLE_F2F),
dip.misc.Help.HelpID.Dialog_NewF2f
);
if(world != null)
{
fileName = null;
setChanged(false);
setSaveEnabled(true);
clientFrame.getClientMenu().setSelected(ClientMenu.EDIT_EDIT_MODE, false);
clientFrame.getClientMenu().setViewNamesNone();
setTitle(world);
// set GameSetup object
world.setGameSetup(new F2FGUIGameSetup());
return world;
}
}
return null;
}// newGame()
/** Saves the current world, if changes have occured. Performs a
* 'Save As' if no file has been set. Returns 'true' if saved ok. */
public boolean save()
{
// only save if changes.
if(clientFrame.getWorld() != null)
{
if(fileName == null)
{
return saveAs();
}
else if(isChanged())
{
return writeGameFile();
}
}
return false;
}// save()
/** Save As: Saves the world after requesting for the filename. Returns 'true' if saved ok.*/
public boolean saveAs()
{
if(clientFrame.getWorld() != null)
{
// JFileChooser setup
XJFileChooser chooser = XJFileChooser.getXJFileChooser();
chooser.addFileFilter(SimpleFileFilter.SAVE_GAME_FILTER);
chooser.setFileFilter(SimpleFileFilter.SAVE_GAME_FILTER);
// set default save-game path
chooser.setCurrentDirectory( GeneralPreferencePanel.getDefaultGameDir() );
// set suggested save name
chooser.setSelectedFile( new File(getSuggestedSaveName()) );
// show dialog
File file = chooser.displaySaveAs(clientFrame);
XJFileChooser.dispose();
// get file name
if(file != null)
{
fileName = file;
boolean returnValue = writeGameFile();
setTitle(); // in case write fails; we have chosen the file name
GeneralPreferencePanel.setRecentFileName(fileName);
clientFrame.getClientMenu().updateRecentFiles();
return returnValue;
}
}
return false;
}// saveAs()
/** Saves the current file to a new file, without changing the currently open file or current state. */
public void saveTo()
{
if(clientFrame.getWorld() != null)
{
// JFileChooser setup
XJFileChooser chooser = XJFileChooser.getXJFileChooser();
chooser.addFileFilter(SimpleFileFilter.SAVE_GAME_FILTER);
chooser.setFileFilter(SimpleFileFilter.SAVE_GAME_FILTER);
chooser.setCurrentDirectory( GeneralPreferencePanel.getDefaultGameDir() );
File file = chooser.displaySave(clientFrame, Utils.getLocalString(SAVE_TO_TITLE));
XJFileChooser.dispose();
// get file name
if(file != null)
{
File saveToFile = file;
try
{
World.save(saveToFile, clientFrame.getWorld());
// DO NOT clear changed flag, though.
// Update recent file name list
GeneralPreferencePanel.setRecentFileName(saveToFile);
clientFrame.getClientMenu().updateRecentFiles();
}
catch(Exception e)
{
ErrorDialog.displayFileIO(clientFrame, e, saveToFile.toString());
}
}
}
}// saveTo()
/** Lets the user choose the judge file to import */
public World importJudge(World currentWorld)
{
// JFileChooser setup
XJFileChooser chooser = XJFileChooser.getXJFileChooser();
chooser.addFileFilter(SimpleFileFilter.TXT_FILTER);
chooser.setFileFilter(SimpleFileFilter.TXT_FILTER);
chooser.setCurrentDirectory( GeneralPreferencePanel.getDefaultGameDir() );
File file = chooser.displayOpen(clientFrame, Utils.getLocalString(IMPORT_CHOOSER_TITLE));
XJFileChooser.dispose();
// get file name
if(file != null)
{
return importJudge(file, currentWorld);
}
return null;
}// importJudge()
/**
* Imports the given Judge file (no file requester dialog is displayed)
* Returns: null, if the current world has been updated, or a new world object
*/
public World importJudge(File file, World currentWorld)
{
World world = null;
// TODO: operate on a backup up currentWorld, if everything is ok, update real currentWorld
// reason for this: if we operate on the real world and something goes wrong,
// then we have got a currupted world!
try
{
JudgeImport ji = new JudgeImport(clientFrame.getGUIOrderFactory(), file, currentWorld);
// Check if the results (if any) matched the current game,
// otherwise diplay dialog and try again
while ((ji.getResult() == JudgeImport.JI_RESULT_TRYREWIND) ||
(ji.getResult() == JudgeImport.JI_RESULT_LOADOTHER))
{
String gameInfo = ji.getGameInfo();
Phase phase = Phase.parse(gameInfo);
if (ji.getResult() == JudgeImport.JI_RESULT_TRYREWIND)
{
// we need to rewind the current game
if (rewindDialog(phase))
{
// rewind current game
Iterator iter = currentWorld.getPhaseSet().iterator();
LinkedList l = new LinkedList();
while(iter.hasNext())
{
Phase p = (Phase) iter.next();
if (p.compareTo(phase) > 0)
{
l.add(p);
}
}
while (!l.isEmpty())
{
Phase p = (Phase)l.getFirst();
l.removeFirst();
TurnState ts = currentWorld.getTurnState(p);
currentWorld.removeTurnState( ts );
}
// clear orders for the last turnstate, because we have got the new orders
currentWorld.getLastTurnState().clearAllOrders();
}
else
{
// abort.
return world;
}
}
else
{
// we need to load the correct game
if (loadDialog(gameInfo))
{
World newWorld = open();
if (newWorld != null)
{
clientFrame.createWorld(newWorld);
currentWorld = clientFrame.getWorld();
}
else
{
// abort.
return world;
}
}
else
{
// abort.
return world;
}
}
// try again.
ji = new JudgeImport(clientFrame.getGUIOrderFactory(), file, currentWorld);
}
if (ji.getResult() == JudgeImport.JI_RESULT_THISWORLD)
{
// show results (if desired)
if(GeneralPreferencePanel.getShowResolutionResults())
{
final TurnState priorTS = clientFrame.getWorld().getPreviousTurnState(clientFrame.getWorld().getLastTurnState());
ResultWriter.displayDialog(clientFrame, priorTS, clientFrame.getOFO());
}
}
if (ji.getResult() == JudgeImport.JI_RESULT_NEWWORLD)
{
if(confirmDialog())
{
world = ji.getWorld();
fileName = null;
setChanged(true);
setSaveEnabled(true);
clientFrame.getClientMenu().setSelected(ClientMenu.EDIT_EDIT_MODE, false);
setTitle(world);
}
} else {
// we modified the current world
setChanged(true);
clientFrame.getClientMenu().setSelected(ClientMenu.EDIT_EDIT_MODE, false);
clientFrame.fireTurnstateChanged(currentWorld.getLastTurnState());
}
clientFrame.fireStateModified();
}
catch(Exception e)
{
ErrorDialog.displayFileIO(clientFrame, e, file.toString());
}
return world;
}// importJudge()
/** Imports a game from Floc.Net. User is prompted for required information. */
public World importFloc()
{
World world = null;
if(confirmDialog())
{
world = FlocImportDialog.displayDialog(clientFrame);
}
return world;
}// importFloc()
/**
* Given a file from a Drag operation, attempt to open it
* as a game file if it has an extension of SimpleFileFilter.SAVE_GAME_FILTER
* type. Otherwise, attempt to import it.
*
*/
public World acceptDrag(File selectedFile, World currentWorld)
{
if(selectedFile.getPath().toLowerCase().endsWith("." + SimpleFileFilter.SAVE_GAME_FILTER.getExtension()))
{
return open(selectedFile);
}
else
{
return importJudge(selectedFile, currentWorld);
}
}// acceptDrag()
// reads in a game file
private World readGameFile(File file)
throws Exception
{
World w = World.open(file);
// check if variant is available; if not, inform user.
World.VariantInfo vi = w.getVariantInfo();
if(VariantManager.getVariant(vi.getVariantName(), vi.getVariantVersion()) == null)
{
Variant variant = VariantManager.getVariant(vi.getVariantName(), VariantManager.VERSION_NEWEST);
if(variant == null)
{
// we don't have the variant AT ALL
ErrorDialog.displayVariantNotAvailable(clientFrame, vi);
return null;
}
else
{
// try most current version: HOWEVER, warn the user that it might not work
ErrorDialog.displayVariantVersionMismatch(clientFrame, vi, variant.getVersion());
vi.setVariantVersion( variant.getVersion() );
}
}
return w;
}// readGameFile()
/** Serialize Game Data to disk. Performs synchronization. */
private boolean writeGameFile()
{
try
{
World w = clientFrame.getWorld();
Log.println("PM::writeGameFile(): saving GUIGameSetup");
// notify the GameSetup object to update its
// state
GUIGameSetup ggs = (GUIGameSetup) w.getGameSetup();
if(ggs != null)
{
ggs.save(clientFrame);
}
// save data, update saved flags
Log.println("PM::writeGameFile(): saving world....");
World.save(fileName, w);
Log.println("PM::writeGameFile(): world saved ok.");
setChanged(false);
return true;
}
catch(Exception e)
{
ErrorDialog.displayFileIO(clientFrame, e, fileName.toString());
}
return false;
}// writeGameFile()
private void setTitle()
{
setTitle(null);
}// setTitle()
/** World object may not yet be available in ClientFrame; if not, can specify it here (or null) */
private void setTitle(World localWorld)
{
StringBuffer title = new StringBuffer(128);
title.append(ClientFrame.getProgramName());
// if no file is open, we shouldn't display a gamename/filename
if(localWorld != null || clientFrame.getWorld() != null)
{
// use local world, if not, use clientFrame world
World world = (localWorld != null) ? localWorld : clientFrame.getWorld();
// get game name
// game name is optional; doesn't have to be the same as the file name
String gameName = world.getGameMetadata().getGameName();
gameName = (EMPTY.equals(gameName)) ? null : gameName;
title.append(" - ");
if(gameName != null)
{
title.append(gameName);
title.append(" [");
}
// title
if(fileName != null)
{
title.append(fileName.getName());
}
else
{
title.append(Utils.getLocalString(UNSAVED_NAME));
}
if(gameName != null)
{
title.append(']');
}
// changed flag
if(Utils.isOSX())
{
// aqua-specific. Draws dot in close button.
// http://developer.apple.com/qa/qa2001/qa1146.html
clientFrame.getRootPane().putClientProperty(WINDOW_MODIFIED, Boolean.valueOf(isChanged));
}
else
{
if(isChanged)
{
title.append(' ');
title.append( Utils.getLocalString(MODIFIED_INDICATOR) );
}
}
}
clientFrame.setTitle(title.toString());
}// setTitle()
private void setSaveEnabled(boolean value)
{
clientFrame.getClientMenu().setEnabled(ClientMenu.FILE_SAVE, value);
clientFrame.getClientMenu().setEnabled(ClientMenu.FILE_SAVEAS, value);
clientFrame.getClientMenu().setEnabled(ClientMenu.FILE_SAVETO, value);
}// setSaveEnabled()
// returns 'true' if can proceed (not cancelled, or after save)
private boolean confirmDialog()
{
if(isChanged())
{
// per apple guidelines:
// [don't save] ==big space=== [cancel] [save]
// we will switch cancel/save to make it more like windows (cancel on right)
Object[] dlgOptions =
{
Utils.getLocalString(CONFIRM_BUTTON_DONTSAVE ), // 0
Box.createRigidArea(new Dimension(25,5)), // 1
Utils.getLocalString(CONFIRM_BUTTON_SAVE), // 2
Utils.getLocalString(CONFIRM_BUTTON_CANCEL) // 3
};
String message = Utils.getText( Utils.getLocalString(CONFIRM_TEXT) );
String title = Utils.getLocalString(CONFIRM_TITLE);
int result = JOptionPane.showOptionDialog(clientFrame, message, title,
JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE,
null, dlgOptions, dlgOptions[3]);
// the result returned corresponds to 0-3, as specified in dlgOptions.
// of course, option 1 (a spacer) cannot be returned.
if(result == 2)
{
// save; however, if save is cancelled, cancel
return save();
}
else
{
return !(result == 3 || result == JOptionPane.CLOSED_OPTION);
}
}
return true;
}// confirmDialog()
private boolean rewindDialog(Phase phase)
{
Object[] dlgOptions =
{
Utils.getLocalString(CONFIRM_BUTTON_REWIND), // 0
Box.createRigidArea(new Dimension(25,5)), // 1
Utils.getLocalString(CONFIRM_BUTTON_CANCEL) // 2
};
String message = Utils.getText( Utils.getLocalString(CONFIRM_REWIND_TEXT), phase.toString());
String title = Utils.getLocalString(CONFIRM_REWIND_TITLE);
int result = JOptionPane.showOptionDialog(clientFrame, message, title,
JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE,
null, dlgOptions, dlgOptions[2]);
// the result returned corresponds to 0-2, as specified in dlgOptions.
// of course, option 1 (a spacer) cannot be returned.
return (result == 0);
}
private boolean loadDialog(String gameInfo)
{
Object[] dlgOptions =
{
Utils.getLocalString(CONFIRM_BUTTON_LOAD ), // 0
Box.createRigidArea(new Dimension(25,5)), // 1
Utils.getLocalString(CONFIRM_BUTTON_CANCEL) // 2
};
String message = Utils.getText( Utils.getLocalString(CONFIRM_LOAD_TEXT), gameInfo);
String title = Utils.getLocalString(CONFIRM_LOAD_TITLE);
int result = JOptionPane.showOptionDialog(clientFrame, message, title,
JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE,
null, dlgOptions, dlgOptions[2]);
// the result returned corresponds to 0-2, as specified in dlgOptions.
// of course, option 1 (a spacer) cannot be returned.
return (result == 0);
}
private void setChanged(boolean value)
{
isChanged = value;
setTitle();
}// setChanged()
/**
* Gets a suggested filename for a saved game. This will be:<br>
* GameName <br>
* VariantName [no GameName] <br>
* SaveFileName [if exists] <br>
* No extension is appended.<br>
* Assumes current World/TurnState are not null.
*/
public String getSuggestedSaveName()
{
if(fileName == null)
{
// game name?
String gameName = clientFrame.getWorld().getGameMetadata().getGameName();
if(!EMPTY.equals(gameName) && gameName != null)
{
return gameName;
}
// use variant name
//
return clientFrame.getWorld().getVariantInfo().getVariantName();
}
else
{
return fileName.getName();
}
}// getSuggestedSaveName()
/**
* Gets a suggested export name. This will be:
* SaveFileName + Phase <br>
* GameName + Phase [if no save file name] <br>
* VariantName + Phase [if no gamename] <br>
* No extension is appended.<br>
* Assumes current World/TurnState are not null.
*/
public String getSuggestedExportName()
{
StringBuffer sb = new StringBuffer(64);
// get prefix
sb.append( getSuggestedSaveName() );
// remove trailing extension from suggested name, if any
int idx = sb.lastIndexOf("."+SimpleFileFilter.SAVE_GAME_FILTER.getExtension());
if(idx >= 0)
{
sb.replace(idx, sb.length(), "");
}
// append brief phase name
sb.append('-');
sb.append( clientFrame.getTurnState().getPhase().getBriefName() );
return sb.toString();
}// getSuggestedExportName()
}// class PersistenceManager