/* * $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: case JOptionPane.CLOSED_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 // Bug 10179 - Global prefs are now written out each time a preference is changed // 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; } }