VASSAL.build.GameModule.java Source code

Java tutorial

Introduction

Here is the source code for VASSAL.build.GameModule.java

Source

/*
 * $Id$
 *
 * Copyright (c) 2000-2012 by Rodney Kinney, Brent Easton
 *
 * 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;

import java.awt.FileDialog;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JToolBar;
import javax.swing.KeyStroke;

import org.apache.commons.codec.digest.DigestUtils;
import org.slf4j.LoggerFactory;

import VASSAL.Info;
import VASSAL.build.module.BasicCommandEncoder;
import VASSAL.build.module.ChartWindow;
import VASSAL.build.module.Chatter;
import VASSAL.build.module.DiceButton;
import VASSAL.build.module.DoActionButton;
import VASSAL.build.module.Documentation;
import VASSAL.build.module.GameState;
import VASSAL.build.module.GlobalKeyCommand;
import VASSAL.build.module.GlobalOptions;
import VASSAL.build.module.Inventory;
import VASSAL.build.module.Map;
import VASSAL.build.module.ModuleExtension;
import VASSAL.build.module.MultiActionButton;
import VASSAL.build.module.NotesWindow;
import VASSAL.build.module.PieceWindow;
import VASSAL.build.module.PlayerHand;
import VASSAL.build.module.PlayerRoster;
import VASSAL.build.module.Plugin;
import VASSAL.build.module.PredefinedSetup;
import VASSAL.build.module.PrivateMap;
import VASSAL.build.module.PrototypesContainer;
import VASSAL.build.module.RandomTextButton;
import VASSAL.build.module.ServerConnection;
import VASSAL.build.module.SpecialDiceButton;
import VASSAL.build.module.StartupGlobalKeyCommand;
import VASSAL.build.module.ToolbarMenu;
import VASSAL.build.module.WizardSupport;
import VASSAL.build.module.documentation.HelpFile;
import VASSAL.build.module.metadata.ModuleMetaData;
import VASSAL.build.module.properties.ChangePropertyCommandEncoder;
import VASSAL.build.module.properties.MutablePropertiesContainer;
import VASSAL.build.module.properties.MutableProperty;
import VASSAL.build.module.properties.PropertySource;
import VASSAL.build.module.turn.TurnTracker;
import VASSAL.build.widget.PieceSlot;
import VASSAL.command.Command;
import VASSAL.command.CommandEncoder;
import VASSAL.command.Logger;
import VASSAL.command.NullCommand;
import VASSAL.configure.CompoundValidityChecker;
import VASSAL.configure.MandatoryComponent;
import VASSAL.counters.GamePiece;
import VASSAL.i18n.ComponentI18nData;
import VASSAL.i18n.Localization;
import VASSAL.i18n.Resources;
import VASSAL.launch.PlayerWindow;
import VASSAL.preferences.Prefs;
import VASSAL.tools.ArchiveWriter;
import VASSAL.tools.ArrayUtils;
import VASSAL.tools.CRCUtils;
import VASSAL.tools.DataArchive;
import VASSAL.tools.KeyStrokeListener;
import VASSAL.tools.KeyStrokeSource;
import VASSAL.tools.NamedKeyStroke;
import VASSAL.tools.ReadErrorDialog;
import VASSAL.tools.ToolBarComponent;
import VASSAL.tools.WarningDialog;
import VASSAL.tools.WriteErrorDialog;
import VASSAL.tools.filechooser.FileChooser;
import VASSAL.tools.image.ImageTileSource;
import VASSAL.tools.image.tilecache.ImageTileDiskCache;
import VASSAL.tools.io.IOUtils;

/**
 * The GameModule class is the base class for a VASSAL module.  It is
 * the root of the {@link Buildable} containment hierarchy.
 * Components which are added directly to the GameModule are contained
 * in the <code>VASSAL.build.module</code> package.
 *
 * <p>It is a singleton, and contains access points for many other classes,
 * such as {@link DataArchive}, {@link ServerConnection}, {@link Logger},
 * and {@link Prefs}.</p>
 */
public abstract class GameModule extends AbstractConfigurable
        implements CommandEncoder, ToolBarComponent, PropertySource, MutablePropertiesContainer, GpIdSupport {
    private static final org.slf4j.Logger log = LoggerFactory.getLogger(GameModule.class);

    protected static final String DEFAULT_NAME = "Unnamed module"; //$NON-NLS-1$
    public static final String MODULE_NAME = "name"; //$NON-NLS-1$
    public static final String MODULE_VERSION = "version"; //$NON-NLS-1$
    public static final String DESCRIPTION = "description";
    public static final String VASSAL_VERSION_CREATED = "VassalVersion"; //$NON-NLS-1$
    /** The System property of this name will return a version identifier for the version of VASSAL being run */
    public static final String VASSAL_VERSION_RUNNING = "runningVassalVersion"; //$NON-NLS-1$
    public static final String NEXT_PIECESLOT_ID = "nextPieceSlotId";
    public static final String BUILDFILE = "buildFile";

    private static GameModule theModule;

    protected String moduleVersion = "0.0"; //$NON-NLS-1$
    protected String vassalVersionCreated = "0.0"; //$NON-NLS-1$
    protected String gameName = DEFAULT_NAME;
    protected String localizedGameName = null;
    protected String description = "";
    protected String lastSavedConfiguration;
    protected FileChooser fileChooser;
    protected FileDialog fileDialog;
    protected MutablePropertiesContainer propsContainer = new Impl();
    protected PropertyChangeListener repaintOnPropertyChange = new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent evt) {
            for (Map map : Map.getMapList()) {
                map.repaint();
            }
        }
    };

    protected PlayerWindow frame = new PlayerWindow();
    protected JPanel controlPanel = frame.getControlPanel();

    protected GameState theState;
    protected DataArchive archive;
    protected Prefs preferences;
    protected Logger logger;
    protected Chatter chat;
    protected Random RNG = new SecureRandom();
    protected ServerConnection server;

    protected ImageTileSource tcache;

    protected WizardSupport wizardSupport;
    protected PropertyChangeSupport idChangeSupport;

    protected List<KeyStrokeSource> keyStrokeSources = new ArrayList<KeyStrokeSource>();
    protected List<KeyStrokeListener> keyStrokeListeners = new ArrayList<KeyStrokeListener>();
    protected CommandEncoder[] commandEncoders = new CommandEncoder[0];
    protected List<String> deferredChat = new ArrayList<String>();

    protected int nextGpId = 0;

    protected boolean loggingPaused = false;
    protected Object loggingLock = new Object();
    protected Command pausedCommands;

    /*
     * Store the currently building GpId source. Only meaningful while
     * the GameModule or an Extension is actually in the process of being built
     * during module/extension load.
     */
    protected GpIdSupport gpidSupport = null;
    protected Long crc = null;

    /**
     * @return the top-level frame of the controls window
     */
    public JFrame getFrame() {
        return frame;
    }

    public void initFrameTitle() {
        frame.setTitle(getLocalizedGameName());
    }

    public WizardSupport getWizardSupport() {
        if (wizardSupport == null) {
            wizardSupport = new WizardSupport();
        }
        return wizardSupport;
    }

    protected GameModule(DataArchive archive) {
        this.archive = archive;

        frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
        frame.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                quit();
            }
        });

        addKeyStrokeSource(new KeyStrokeSource(frame.getRootPane(), JComponent.WHEN_IN_FOCUSED_WINDOW));

        validator = new CompoundValidityChecker(new MandatoryComponent(this, Documentation.class),
                new MandatoryComponent(this, GlobalOptions.class));

        addCommandEncoder(new ChangePropertyCommandEncoder(propsContainer));
    }

    /**
     * Initialize the module
     */
    protected abstract void build() throws IOException;

    public void setAttribute(String name, Object value) {
        if (MODULE_NAME.equals(name)) {
            if (Localization.getInstance().isTranslationInProgress()) {
                localizedGameName = (String) value;
            } else {
                gameName = (String) value;
            }
            setConfigureName((String) value);
        } else if (MODULE_VERSION.equals(name)) {
            moduleVersion = (String) value;
        } else if (VASSAL_VERSION_CREATED.equals(name)) {
            vassalVersionCreated = (String) value;
            String runningVersion = Info.getVersion();
            if (Info.compareVersions(vassalVersionCreated, runningVersion) > 0) {
                WarningDialog.show("GameModule.version_warning", vassalVersionCreated, runningVersion);
            }
        } else if (NEXT_PIECESLOT_ID.equals(name)) {
            try {
                nextGpId = Integer.parseInt((String) value);
            } catch (NumberFormatException e) {
                throw new IllegalBuildException(e);
            }
        } else if (DESCRIPTION.equals(name)) {
            description = (String) value;
        }
    }

    public String getAttributeValueString(String name) {
        if (MODULE_NAME.equals(name)) {
            return gameName;
        } else if (MODULE_VERSION.equals(name)) {
            return moduleVersion;
        } else if (VASSAL_VERSION_CREATED.equals(name)) {
            return vassalVersionCreated;
        } else if (VASSAL_VERSION_RUNNING.equals(name)) {
            return Info.getVersion();
        } else if (NEXT_PIECESLOT_ID.equals(name)) {
            return String.valueOf(nextGpId);
        } else if (DESCRIPTION.equals(name)) {
            return description;
        }
        return null;
    }

    /**
     *
     * A valid verson format is "w.x.y[bz]", where
     * 'w','x','y', and 'z' are integers.
     * @return a negative number if <code>v2</code> is a later version
     * the <code>v1</code>, a positive number if an earlier version,
     * or zero if the versions are the same.
     *
     * @deprecated use {@link Info#compareVersions}
     */
    @Deprecated
    public static int compareVersions(String v1, String v2) {
        return Info.compareVersions(v1, v2);
    }

    public void addTo(Buildable b) {
    }

    public static String getConfigureTypeName() {
        return Resources.getString("Editor.GameModule.component_type"); //$NON-NLS-1$
    }

    public void removeFrom(Buildable parent) {
    }

    public HelpFile getHelpFile() {
        return HelpFile.getReferenceManualPage("GameModule.htm"); //$NON-NLS-1$
    }

    public String[] getAttributeNames() {
        return new String[] { MODULE_NAME, MODULE_VERSION, DESCRIPTION, VASSAL_VERSION_CREATED, NEXT_PIECESLOT_ID };
    }

    public String[] getAttributeDescriptions() {
        return new String[] { Resources.getString("Editor.GameModule.name_label"), //$NON-NLS-1$
                Resources.getString("Editor.GameModule.version_label"), //$NON-NLS-1$
                Resources.getString("Editor.GameModule.description") };
    }

    public Class<?>[] getAttributeTypes() {
        return new Class<?>[] { String.class, String.class, String.class };
    }

    public Class<?>[] getAllowableConfigureComponents() {
        return new Class<?>[] { Map.class, PieceWindow.class, PrototypesContainer.class, ToolbarMenu.class,
                MultiActionButton.class, DoActionButton.class, DiceButton.class, GlobalKeyCommand.class,
                StartupGlobalKeyCommand.class, Inventory.class,
                //                  InternetDiceButton.class,   // Disable internet dice button until Bones server can prevent email spamming
                RandomTextButton.class, SpecialDiceButton.class, PredefinedSetup.class, ChartWindow.class,
                PrivateMap.class, PlayerHand.class, NotesWindow.class, TurnTracker.class };
    }

    /**
     * The GameModule acts as the mediator for hotkey events.
     *
     * Components that wish to fire hotkey events when they have the
     * focus should register themselves using this method.  These events will be
     * forwarded to all listeners that have registered themselves with {@link #addKeyStrokeListener}
     */
    public void addKeyStrokeSource(KeyStrokeSource src) {
        keyStrokeSources.add(src);
        for (KeyStrokeListener l : keyStrokeListeners) {
            l.addKeyStrokeSource(src);
        }
    }

    /**
     * The GameModule acts as the mediator for hotkey events.
     *
     * Objects that react to hotkey events should register themselves
     * using this method.  Any component that has been registered with {@link #addKeyStrokeSource}
     * will forward hotkey events to listeners registered with this method.
     */
    public void addKeyStrokeListener(KeyStrokeListener l) {
        keyStrokeListeners.add(l);
        for (KeyStrokeSource s : keyStrokeSources) {
            l.addKeyStrokeSource(s);
        }
    }

    @Deprecated
    public void fireKeyStroke(KeyStroke stroke) {
        if (stroke != null) {
            for (KeyStrokeListener l : keyStrokeListeners) {
                l.keyPressed(stroke);
            }
        }
    }

    public void fireKeyStroke(NamedKeyStroke stroke) {
        if (stroke != null && !stroke.isNull()) {
            fireKeyStroke(stroke.getKeyStroke());
        }
    }

    /**
     * @return the name of the game for this module
     */
    public String getGameName() {
        return gameName;
    }

    public String getLocalizedGameName() {
        return localizedGameName == null ? gameName : localizedGameName;
    }

    public String getGameVersion() {
        return moduleVersion;
    }

    /** The {@link Prefs} key for the user's real name */
    public static final String REAL_NAME = "RealName"; //$NON-NLS-1$
    /** The {@link Prefs} key for the user's secret name */
    public static final String SECRET_NAME = "SecretName"; //$NON-NLS-1$
    /** The {@link Prefs} key for the user's personal info */
    public static final String PERSONAL_INFO = "Profile"; //$NON-NLS-1$

    public void addIdChangeListener(PropertyChangeListener l) {
        idChangeSupport.addPropertyChangeListener(l);
    }

    public void removeIdChangeListener(PropertyChangeListener l) {
        idChangeSupport.removePropertyChangeListener(l);
    }

    /**
     * @return the preferences for this module
     */
    public Prefs getPrefs() {
        if (preferences == null) {
            setPrefs(new Prefs(Prefs.getGlobalPrefs().getEditor(), gameName));
        }
        return preferences;
    }

    /**
     * A set of preferences that applies to all modules
     * @return
     */
    @Deprecated
    public Prefs getGlobalPrefs() {
        return Prefs.getGlobalPrefs();
    }

    /**
     * This method adds a {@link CommandEncoder} to the list of objects
     * that will attempt to decode/encode a command
     *
     * @see #decode
     * @see #encode
     */
    public void addCommandEncoder(CommandEncoder ce) {
        commandEncoders = ArrayUtils.append(commandEncoders, ce);
    }

    /**
     * This method removes a {@link CommandEncoder} from the list of objects
     * that will attempt to decode/encode a command
     *
     *
     * @see #decode
     * @see #encode
     */
    public void removeCommandEncoder(CommandEncoder ce) {
        commandEncoders = ArrayUtils.remove(commandEncoders, ce);
    }

    /**
     * Central location to create any type of GamePiece from within VASSAL
     *
     * @param type
     * @return
     */
    public GamePiece createPiece(String type) {
        for (int i = 0; i < commandEncoders.length; ++i) {
            if (commandEncoders[i] instanceof BasicCommandEncoder) {
                GamePiece p = ((BasicCommandEncoder) commandEncoders[i]).createPiece(type);
                if (p != null) {
                    return p;
                }
            }
        }
        return null;
    }

    public GamePiece createPiece(String type, GamePiece inner) {
        for (int i = 0; i < commandEncoders.length; ++i) {
            if (commandEncoders[i] instanceof BasicCommandEncoder) {
                GamePiece p = ((BasicCommandEncoder) commandEncoders[i]).createDecorator(type, inner);
                if (p != null) {
                    return p;
                }
            }
        }
        return null;
    }

    /**
     * Display the given text in the control window's status line.
     * Save the messages for later if the Chatter has not been initialised yet
     */
    public void warn(String s) {
        if (chat == null) {
            deferredChat.add(s);
        } else {
            chat.show(" - " + s); //$NON-NLS-1$
        }
    }

    /**
     * @return a single Random number generator that all objects may share
     */
    public Random getRNG() {
        return RNG;
    }

    /**
     * @return the object responsible for logging commands to a logfile
     */
    public Logger getLogger() {
        return logger;
    }

    /**
     * Set the object that displays chat text. Display any warning
     * messages deferred during earlier initialisation
     */
    public void setChatter(Chatter c) {
        chat = c;
        if (deferredChat.size() > 0) {
            for (String msg : deferredChat) {
                warn(msg);
            }
            deferredChat.clear();
        }
    }

    public JComponent getControlPanel() {
        return controlPanel;
    }

    /**
     * @return the object that displays chat text
     */
    public Chatter getChatter() {
        return chat;
    }

    public void setPrefs(Prefs p) {
        preferences = p;
        preferences.getEditor().initDialog(getFrame());
    }

    @Deprecated
    public void setGlobalPrefs(Prefs p) {
    }

    /**
     * Uses the registered  {@link CommandEncoder}s
     * to decode a String into a {@link Command}.
     */
    public Command decode(String command) {
        if (command == null) {
            return null;
        } else {
            Command c = null;
            for (int i = 0; i < commandEncoders.length && c == null; ++i) {
                c = commandEncoders[i].decode(command);
            }
            if (c == null) {
                System.err.println("Failed to decode " + command); //$NON-NLS-1$
            }
            return c;
        }
    }

    /**
     * Uses the registered {@link CommandEncoder}s to encode a {@link Command} into a String object
     */
    public String encode(Command c) {
        if (c == null) {
            return null;
        }
        String s = null;
        for (int i = 0; i < commandEncoders.length && s == null; ++i) {
            s = commandEncoders[i].encode(c);
        }
        if (s == null) {
            System.err.println("Failed to encode " + c); //$NON-NLS-1$
        }
        return s;
    }

    /**
     * @return a common FileChooser so that recent file locations
     * can be remembered
     */
    public FileChooser getFileChooser() {
        if (fileChooser == null) {
            fileChooser = FileChooser.createFileChooser(getFrame(),
                    getGameState().getSavedGameDirectoryPreference());
        } else {
            fileChooser.resetChoosableFileFilters();
            fileChooser.rescanCurrentDirectory();
        }

        return fileChooser;
    }

    /**
     * @deprecated Use {@link #getFileChooser} instead.
     */
    @Deprecated
    public FileDialog getFileDialog() {
        if (fileDialog == null) {
            fileDialog = new FileDialog(getFrame());
            File f = getGameState().getSavedGameDirectoryPreference().getFileValue();
            if (f != null) {
                fileDialog.setDirectory(f.getPath());
            }
            fileDialog.setModal(true);
        } else {
            fileDialog.setDirectory(fileDialog.getDirectory());
        }
        return fileDialog;
    }

    /**
     * @return the JToolBar of the command window
     */
    public JToolBar getToolBar() {
        return frame.getToolBar();
    }

    /**
     * Append the string to the title of the controls window and all Map windows
     * @param s If null, set the title to the default.
     */
    public void appendToTitle(String s) {
        if (s == null) {
            frame.setTitle(Resources.getString("GameModule.frame_title", getLocalizedGameName())); //$NON-NLS-1$
        } else {
            frame.setTitle(frame.getTitle() + s);
        }
        for (Map m : getComponentsOf(Map.class)) {
            m.appendToTitle(s);
        }
    }

    /**
     * Exit the application, prompting user to save if necessary
     */
    public void quit() {
        if (shutDown()) {
            System.exit(0);
        }
    }

    /**
     * Prompt user to save open game and modules/extensions being edited
     * @return true if shutDown should proceed, i.e. user did not cancel
     */
    public boolean shutDown() {
        boolean cancelled;
        getGameState().setup(false);
        cancelled = getGameState().isGameStarted();

        if (!cancelled) {
            if (getDataArchive() instanceof ArchiveWriter && !buildString().equals(lastSavedConfiguration)) {
                switch (JOptionPane.showConfirmDialog(frame, Resources.getString("GameModule.save_module"), //$NON-NLS-1$
                        "", JOptionPane.YES_NO_CANCEL_OPTION)) { //$NON-NLS-1$
                case JOptionPane.YES_OPTION:
                    save();
                    break;
                case JOptionPane.CANCEL_OPTION:
                    cancelled = true;
                }
            }
            for (ModuleExtension ext : getComponentsOf(ModuleExtension.class)) {
                cancelled = !ext.confirmExit();
            }
        }

        if (!cancelled) {
            Prefs p = null;

            // write and close module prefs
            try {
                p = getPrefs();
                p.write();
                p.close();
            } catch (IOException e) {
                WriteErrorDialog.error(e, p.getFile());
            } finally {
                IOUtils.closeQuietly(p);
            }

            // write and close global prefs
            try {
                p = getGlobalPrefs();
                p.write();
                p.close();
            } catch (IOException e) {
                WriteErrorDialog.error(e, p.getFile());
            } finally {
                IOUtils.closeQuietly(p);
            }

            // close the module
            try {
                archive.close();
            } catch (IOException e) {
                ReadErrorDialog.error(e, archive.getName());
            }

            log.info("Exiting");
        }

        return !cancelled;
    }

    /**
     * Encode the {@link Command}, send it to the server and write it
     * to a logfile (if any is open)
     *
     * @see #encode
     */
    public void sendAndLog(Command c) {
        if (c != null && !c.isNull()) {
            synchronized (loggingLock) {
                if (loggingPaused) {
                    if (pausedCommands == null) {
                        pausedCommands = c;
                    } else {
                        pausedCommands.append(c);
                    }
                } else {
                    getServer().sendToOthers(c);
                    getLogger().log(c);
                }
            }
        }
    }

    /**
     * Pause logging and return true if successful.
     * Return false if logging already paused
     *
     * While Paused, commands are accumulated into pausedCommands so that they
     * can all be logged at the same time, and generate a single UNDO command.
     *
     * @return
     */
    public boolean pauseLogging() {
        synchronized (loggingLock) {
            if (loggingPaused) {
                return false;
            }
            loggingPaused = true;
            pausedCommands = null;
            return true;
        }
    }

    /**
     * Restart logging and return any outstanding commands
     */
    public Command resumeLogging() {
        Command c = null;
        synchronized (loggingLock) {
            c = pausedCommands == null ? new NullCommand() : pausedCommands;
            pausedCommands = null;
            loggingPaused = false;
        }
        return c;
    }

    /**
     * Clear outstanding Commands
     * Use where the calling level handles the sending of outstanding commands
     */
    public void clearPausedCommands() {
        pausedCommands = null;
    }

    private static String userId = null;

    /**
     * @return a String that uniquely identifies the user
     */
    public static String getUserId() {
        return userId;
    }

    /**
     * Set the identifier for the user
     */
    public static void setUserId(String newId) {
        userId = newId;
    }

    /**
     * @return the object reponsible for sending messages to the server
     */
    public ServerConnection getServer() {
        return server;
    }

    /**
     * Set the singleton GameModule and invoke {@link #build} on it.
     */
    public static void init(GameModule module) throws IOException {
        if (theModule != null) {
            throw new UnsupportedOperationException(
                    Resources.getString("GameModule.open_error", theModule.getDataArchive().getName()));
        } else {
            theModule = module;
            theModule.setGpIdSupport(theModule);
            try {
                theModule.build();
            } catch (IOException e) {
                theModule = null;
                throw e;
            }
        }

        /*
         *  If we are editing, check for duplicate, illegal or missing GamePiece Id's
         *  and update if necessary.
         */
        if (theModule.getDataArchive() instanceof ArchiveWriter) {
            theModule.checkGpIds();
        }

        /*
         * Tell any Plugin components that the build is complete so that they
         * can finish initialization.
         */
        for (Plugin plugin : theModule.getComponentsOf(Plugin.class)) {
            plugin.init();
        }
    }

    /**
     * Unload the module
     */
    public static void unload() {
        if (theModule != null) {
            if (theModule.shutDown()) {
                theModule = null;
            }
        }
    }

    // Saved the current buildString for comparison when we try and quit.
    public void updateLastSave() {
        lastSavedConfiguration = buildString();
    }

    public String generateGpId() {
        return String.valueOf(nextGpId++);
    }

    public int getNextGpId() {
        return nextGpId;
    }

    public void setNextGpId(int id) {
        nextGpId = id;
    }

    public void setGpIdSupport(GpIdSupport s) {
        gpidSupport = s;
    }

    public GpIdSupport getGpIdSupport() {
        return gpidSupport;
    }

    /**
     * Check every PieceSlot and PlaceMarker trait for duplicate,
     * illegal or Missing GamePiece id's and update them if necessary
     */
    protected void checkGpIds() {
        final GpIdChecker checker = new GpIdChecker(this);
        for (PieceSlot pieceSlot : theModule.getAllDescendantComponentsOf(PieceSlot.class)) {
            checker.add(pieceSlot);
        }
        checker.fixErrors();
    }

    /**
     * @return the object which stores data for the module
     */
    public DataArchive getDataArchive() {
        return archive;
    }

    /**
     * If the module is being edited, return the writeable archive for the module
     */
    public ArchiveWriter getArchiveWriter() {
        return archive.getWriter();
    }

    public ImageTileSource getImageTileSource() {
        if (tcache == null) {
            // FIXME: There's no guarantee that getGameName() and getGameVersion()
            // are properly set at this point.

            final String hstr = DigestUtils.shaHex(getGameName() + "_" + getGameVersion());

            final File tc = new File(Info.getConfDir(), "tiles/" + hstr);
            tcache = new ImageTileDiskCache(tc.getAbsolutePath());
        }

        return tcache;
    }

    /**
     * Is the module being translated into the user's Locale?  Localization is disabled when editing a module
     *
     * @return true if the module/extension has been localized
     */
    public boolean isLocalizationEnabled() {
        return getArchiveWriter() == null;
    }

    /**
     * @return the singleton instance of GameModule
     */
    public static GameModule getGameModule() {
        return theModule;
    }

    /**
     * Return the object responsible for tracking the state of a game.
     * Only one game in progress is allowed;
     */
    public GameState getGameState() {
        return theState;
    }

    public void saveAs() {
        save(true);
    }

    /**
     * If the module is being edited, write the module data
     */
    public void save() {
        save(false);
    }

    protected void save(boolean saveAs) {
        vassalVersionCreated = Info.getVersion();

        final ArchiveWriter writer = getArchiveWriter();

        try {
            (new ModuleMetaData(this)).save(writer);
        } catch (IOException e) {
            WriteErrorDialog.error(e, writer.getName());
        }

        try {
            final String save = buildString();
            writer.addFile(BUILDFILE, new ByteArrayInputStream(save.getBytes("UTF-8"))); //$NON-NLS-1$

            if (saveAs)
                writer.saveAs(true);
            else
                writer.save(true);

            lastSavedConfiguration = save;
        } catch (IOException e) {
            WriteErrorDialog.error(e, writer.getName());
        }
    }

    protected String buildString() {
        org.w3c.dom.Document doc = Builder.createNewDocument();
        doc.appendChild(getBuildElement(doc));
        return Builder.toString(doc);
    }

    /**
     * Return values of Global properties
     */
    public Object getProperty(Object key) {
        if (GlobalOptions.PLAYER_SIDE.equals(key) || GlobalOptions.PLAYER_SIDE_ALT.equals(key)) {
            String mySide = PlayerRoster.getMySide();
            return mySide == null ? "" : mySide; //$NON-NLS-1$
        } else if (GlobalOptions.PLAYER_NAME.equals(key) || GlobalOptions.PLAYER_NAME_ALT.equals(key)) {
            return getPrefs().getValue(GameModule.REAL_NAME);
        } else if (GlobalOptions.PLAYER_ID.equals(key) || GlobalOptions.PLAYER_ID_ALT.equals(key)) {
            return GlobalOptions.getInstance().getPlayerId();
        }
        MutableProperty p = propsContainer.getMutableProperty(String.valueOf(key));
        return p == null ? null : p.getPropertyValue();
    }

    public MutableProperty getMutableProperty(String name) {
        return propsContainer.getMutableProperty(name);
    }

    public void addMutableProperty(String key, MutableProperty p) {
        propsContainer.addMutableProperty(key, p);
        p.addMutablePropertyChangeListener(repaintOnPropertyChange);
    }

    public MutableProperty removeMutableProperty(String key) {
        MutableProperty p = propsContainer.removeMutableProperty(key);
        if (p != null) {
            p.removeMutablePropertyChangeListener(repaintOnPropertyChange);
        }
        return p;
    }

    public String getMutablePropertiesContainerId() {
        return "Module";
    }

    public Object getLocalizedProperty(Object key) {
        if (GlobalOptions.PLAYER_SIDE.equals(key) || GlobalOptions.PLAYER_SIDE_ALT.equals(key)) {
            String mySide = PlayerRoster.getMyLocalizedSide();
            return mySide == null ? "" : mySide; //$NON-NLS-1$
        } else {
            return getProperty(key);
        }
    }

    public long getCrc() {
        if (crc == null) {
            crc = buildCrc();
        }
        return crc.longValue();
    }

    protected Long buildCrc() {
        final List<File> files = new ArrayList<File>();
        if (getDataArchive().getArchive() != null) {
            files.add(new File(getDataArchive().getName()));
        }

        for (ModuleExtension ext : getComponentsOf(ModuleExtension.class)) {
            if (ext.getDataArchive().getArchive() != null) {
                files.add(new File(ext.getDataArchive().getName()));
            }
        }

        try {
            return CRCUtils.getCRC(files);
        } catch (IOException e) {
            log.error("Error generating CRC", e);
            return 0L;
        }
    }

    public ComponentI18nData getI18nData() {
        ComponentI18nData myI18nData = super.getI18nData();
        myI18nData.setAttributeTranslatable(MODULE_VERSION, false);
        return myI18nData;
    }
}