diff --git a/.gitignore b/.gitignore index fd978620d0..86ae2fd2bd 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ out/ target/ releases/ snapshots/ + +# MacOS +.DS_Store diff --git a/vassal-app/src/main/java/VASSAL/build/GpIdChecker.java b/vassal-app/src/main/java/VASSAL/build/GpIdChecker.java index e0c4fffddf..408ad32d68 100644 --- a/vassal-app/src/main/java/VASSAL/build/GpIdChecker.java +++ b/vassal-app/src/main/java/VASSAL/build/GpIdChecker.java @@ -51,6 +51,7 @@ public class GpIdChecker { protected GpIdSupport gpIdSupport; protected int maxId; + protected int noGpIdMatch = 0; // shared to GameRefresher protected boolean extensionsLoaded = false; final Map goodSlots = new HashMap<>(); final List errorSlots = new ArrayList<>(); @@ -75,6 +76,10 @@ public GpIdChecker(Set options) { } } + public int getNoGpIdMatch() { + return noGpIdMatch; + } + public boolean useLabelerName() { return refresherOptions.contains(GameRefresher.USE_LABELER_NAME); //$NON-NLS-1$ } @@ -87,6 +92,9 @@ public boolean useRotateName() { public boolean useName() { return refresherOptions.contains(GameRefresher.USE_NAME); //$NON-NLS-1$ } + public boolean fixGPID() { + return refresherOptions.contains(GameRefresher.FIX_GPID); //$NON-NLS-1$ + } /** * Add a PieceSlot to our cross-reference and any PlaceMarker @@ -160,7 +168,7 @@ protected void testGpId(String id, SlotElement element) { * If this has been called from a ModuleExtension, the GpId is prefixed with * the Extension Id. Remove the Extension Id and just process the numeric part. * - * NOTE: If GpIdChecker is being used by the GameRefesher, then there may be + * NOTE: If GpIdChecker is being used by the GameRefresher, then there may be * extensions loaded, so retain the extension prefix to ensure a correct * unique slot id check. */ @@ -249,7 +257,8 @@ public GamePiece createUpdatedPiece(GamePiece oldPiece) { } } - // Failed to find a slot by gpid, try by matching piece name if option selected + // Failed to find a slot by gpid, try by matching piece name if option selected; always report in summaries + noGpIdMatch++; if (useName()) { final String oldPieceName = Decorator.getInnermost(oldPiece).getName(); for (final SlotElement element : goodSlots.values()) { @@ -258,11 +267,19 @@ public GamePiece createUpdatedPiece(GamePiece oldPiece) { if (oldPieceName.equals(gpName)) { newPiece = element.createPiece(oldPiece, this); copyState(oldPiece, newPiece); + if (fixGPID()) { + newPiece.setProperty(Properties.PIECE_ID, slotPiece.getProperty(Properties.PIECE_ID)); + } + chat("!" + Resources.getString("GpIdChecker.refreshByName", oldPieceName, gpid, slotPiece.getProperty(Properties.PIECE_ID)) + + (fixGPID() ? " " + Resources.getString("GpIdChecker.fixGPID") + "" : "")); return newPiece; } } + chat(GameRefresher.ERROR_MESSAGE_PREFIX + Resources.getString("GpIdChecker.refreshByNameFail", oldPieceName, gpid == null ? "" : gpid)); + } + else { + chat(GameRefresher.ERROR_MESSAGE_PREFIX + Resources.getString("GpIdChecker.SlotNotFound", Decorator.getInnermost(oldPiece).getName(), gpid == null ? "" : gpid)); } - return oldPiece; } diff --git a/vassal-app/src/main/java/VASSAL/build/module/GameRefresher.java b/vassal-app/src/main/java/VASSAL/build/module/GameRefresher.java index 30f4ac4f97..a8cfe393c2 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/GameRefresher.java +++ b/vassal-app/src/main/java/VASSAL/build/module/GameRefresher.java @@ -47,6 +47,7 @@ import VASSAL.i18n.Resources; import VASSAL.tools.BrowserSupport; import VASSAL.tools.ErrorDialog; +import VASSAL.tools.NamedKeyStroke; import VASSAL.tools.swing.FlowLabel; import VASSAL.tools.swing.SwingUtils; import net.miginfocom.swing.MigLayout; @@ -60,10 +61,9 @@ import javax.swing.JCheckBox; import javax.swing.JDialog; import javax.swing.JPanel; +import javax.swing.JSeparator; import javax.swing.JTextArea; import javax.swing.WindowConstants; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.WindowAdapter; @@ -93,24 +93,37 @@ public final class GameRefresher implements CommandEncoder, GameComponent { public static final String COMMAND_PREFIX = "DECKREPOS" + DELIMITER; //$NON-NLS-1$ public static final String USE_NAME = "UseName"; + public static final String FIX_GPID = "fixGPID"; public static final String USE_LABELER_NAME = "UseLabelerName"; public static final String USE_LAYER_NAME = "UseLayerName"; public static final String USE_ROTATE_NAME = "UseRotateName"; public static final String TEST_MODE = "TestMode"; public static final String DELETE_NO_MAP = "DeleteNoMap"; public static final String REFRESH_DECKS = "RefreshDecks"; + public static final String REFRESH_PIECES = "RefreshPieces"; public static final String DELETE_OLD_DECKS = "DeleteOldDecks"; public static final String ADD_NEW_DECKS = "AddNewDecks"; + public static final String USE_HOTKEY = "UseHotkey"; + public static final String SUPPRESS_INFO_REPORTS = "SuppressInfoReports"; private Action refreshAction; private final GpIdSupport gpIdSupport; private GpIdChecker gpIdChecker; - private RefreshDialog dialog; - private int updatedCount; - private int notFoundCount; - private int noStackCount; - private int noMapCount; + private int updatedCount; + private int totalCount; + private int totalDecks; + + private int notFoundCount; // shared to PDS refresher + private int noGpIdMatch; // shared to PDS refresher + private int noStackCount; // shared to PDS refresher - not used!!! + private int noMapCount; // shared to PDS refresher - not used!!! + private int notOwnedCount; // shared to PDS refresher + private int notVisibleCount; // shared to PDS refresher + private int deckWarnings; // shared to PDS refresher + + public static final String ERROR_MESSAGE_PREFIX = "~"; + public static final String SEPARATOR = "----------"; private final GameModule theModule; private final Set options = new HashSet<>(); @@ -127,6 +140,11 @@ public GameRefresher(GpIdSupport gpIdSupport) { theModule = GameModule.getGameModule(); } + public int warnings() { + // Make count of all data warnings available to PreDefinedSetup too + return notFoundCount + noStackCount + noMapCount + notOwnedCount + notVisibleCount + noGpIdMatch + deckWarnings; + } + @Override public String encode(final Command c) { return null; @@ -164,21 +182,19 @@ public Action getRefreshAction() { } public boolean isTestMode() { - return options.contains("TestMode"); //$NON-NLS-1$ + return options.contains(TEST_MODE); //$NON-NLS-1$ } public boolean isDeleteNoMap() { - return options.contains("DeleteNoMap"); //$NON-NLS-1$ + return options.contains(DELETE_NO_MAP); //$NON-NLS-1$ } public void start() { - dialog = new RefreshDialog(this); + final RefreshDialog dialog = new RefreshDialog(this); dialog.setVisible(true); - dialog = null; } public void log(String message) { - // ex for dialog msg dialog.addMessage(Resources.getString("GameRefresher.counters_refreshed_test", updatedCount)); // Log to chatter GameModule.getGameModule().warn(message); logger.info(message); @@ -193,14 +209,11 @@ public void log(String message) { * - Mat with contained Cargo * - Single non-Mat unstacked piece * - * @return + * @return refreshables List of refreshable items */ public List getRefreshables() { final List refreshables = new ArrayList<>(); final List loadedMats = new ArrayList<>(); - int totalCount = 0; - int notOwnedCount = 0; - int notVisibleCount = 0; // Process map by map for (final Map map : Map.getMapList()) { @@ -213,6 +226,7 @@ public List getRefreshables() { final Deck deck = (Deck) piece; totalCount += deck.getPieceCount(); refreshables.add(new DeckRefresher(deck)); + totalDecks++; } // A standard Stack @@ -232,14 +246,14 @@ else if (piece instanceof Stack) { } } } - if (((Stack) piece).getMap() != null) { + if (piece.getMap() != null) { refreshables.add(new StackRefresher((Stack) piece)); } } // An Unstacked piece else { - final GamePiece p = (GamePiece) piece; + final GamePiece p = piece; // Only visible, unobscured pieces are refreshable if (!Boolean.TRUE.equals(piece.getProperty(Properties.INVISIBLE_TO_ME)) @@ -269,21 +283,13 @@ else if (piece instanceof Stack) { } } - // If there are any loaded Mats, then find the Stacks of their cargo in the general Refeshables list, + // If there are any loaded Mats, then find the Stacks of their cargo in the general Refreshables list, // remove them and add them to the MatRefresher. for (final MatRefresher mr : loadedMats) { mr.grabMyCargo(refreshables); } - } - log(Resources.getString("GameRefresher.get_all_pieces")); - log(Resources.getString("GameRefresher.counters_total", totalCount)); - log(Resources.getString("GameRefresher.counters_kept", totalCount - notOwnedCount - notVisibleCount)); - log(Resources.getString("GameRefresher.counters_not_owned", notOwnedCount)); - log(Resources.getString("GameRefresher.counters_not_visible", notVisibleCount)); - log("-"); //$NON-NLS-1$ - return refreshables; } @@ -302,7 +308,8 @@ private boolean isGameActive() { * @throws IllegalBuildException - if we get a gpIdChecker error */ public void execute(Set options, Command command) throws IllegalBuildException { - final List decks = new ArrayList<>(); + + // removed as not use - final List decks = new ArrayList<>(); if (command == null) { command = new NullCommand(); @@ -310,10 +317,14 @@ public void execute(Set options, Command command) throws IllegalBuildExc if (!options.isEmpty()) { this.options.addAll(options); } + + totalCount = 0; + totalDecks = 0; notFoundCount = 0; updatedCount = 0; noMapCount = 0; noStackCount = 0; + /* * 1. Use the GpIdChecker to build a cross-reference of all available * PieceSlots and PlaceMarker's in the module. @@ -331,7 +342,7 @@ public void execute(Set options, Command command) throws IllegalBuildExc if (gpIdChecker.hasErrors()) { // Any gpid errors should have been resolved by the GpId check when the editor is run. - // If a module created before gpIDChecker was setup is run on a vassal version with gmIDChecker + // If a module created before gpIDChecker was set up is run on a vassal version with gmIDChecker // is run in the player, errors might still be present. // Inform user that he must upgrade the module to the latest vassal version before running Refresh gpIdChecker = null; @@ -340,43 +351,54 @@ public void execute(Set options, Command command) throws IllegalBuildExc } } - /* - * 2. Build a list in visual order of all stacks, decks, mats and other pieces that need refreshing - */ + /* + * 2. Build a list in visual order of all stacks, decks, mats and other pieces that need refreshing + */ final List refreshables = getRefreshables(); - /* - * And refresh them. Keep a list of the Decks in case we need to update their attributes - */ + /* + * And refresh them. Even if Refresh Pieces is off, still scan to make a list of the Decks in case we need to update their attributes + */ + if (!options.contains(SUPPRESS_INFO_REPORTS)) log(Resources.getString("GameRefresher.run_refresh_counters_v4")); + + if (!options.contains(SUPPRESS_INFO_REPORTS)) log(Resources.getString("GameRefresher.counters_kept", totalCount - notOwnedCount - notVisibleCount)); + + if (notOwnedCount > 0) + log(ERROR_MESSAGE_PREFIX + Resources.getString("GameRefresher.counters_not_owned", notOwnedCount)); + if (notVisibleCount > 0) + log(ERROR_MESSAGE_PREFIX + Resources.getString("GameRefresher.counters_not_visible", notVisibleCount)); + indexAllAttachments(); - for (final Refresher refresher : refreshables) { + + for (final Refresher refresher : refreshables) refresher.refresh(command); - if (refresher instanceof DeckRefresher) { - decks.add(((DeckRefresher) refresher).getDeck()); - } - } - refreshAllAttachments(command); - log(Resources.getString("GameRefresher.run_refresh_counters_v3", theModule.getGameVersion())); - log(Resources.getString("GameRefresher.counters_refreshed", updatedCount)); - log(Resources.getString("GameRefresher.counters_not_found", notFoundCount)); - log(Resources.getString("GameRefresher.counters_no_map", noMapCount)); - log("----------"); //$NON-NLS-1$ - log(Resources.getString("GameRefresher.counters_no_stack", noStackCount)); - log("----------"); //$NON-NLS-1$ + /* removed as decks array is not used - to implement this code needs to be restored within for loop and pieces / decks refresh conditions interleaved + if (refresher instanceof DeckRefresher && options.contains(REFRESH_DECKS)) { + decks.add(((DeckRefresher) refresher).getDeck()); + }*/ + + refreshAllAttachments(command); + if (!options.contains(SUPPRESS_INFO_REPORTS)) log(Resources.getString("GameRefresher.counters_refreshed", updatedCount)); + if (notFoundCount > 0) + log(ERROR_MESSAGE_PREFIX + Resources.getString("GameRefresher.counters_not_found", notFoundCount)); + if (noMapCount > 0) + log(ERROR_MESSAGE_PREFIX + Resources.getString("GameRefresher.counters_no_map", noMapCount)); + if (noStackCount > 0) + log(ERROR_MESSAGE_PREFIX + Resources.getString("GameRefresher.counters_no_stack", noStackCount)); /* - * 4/ Refresh properties of decks in the game + * 3. Refresh properties of decks in the game */ - if (options.contains("RefreshDecks")) { //NON-NLS + if (options.contains(REFRESH_DECKS)) { //NON-NLS if (isGameActive()) { - // If somebody feels like packaging all these things into Commands, help yourself... - log(Resources.getString("GameRefresher.deck_refresh_during_multiplayer")); + // FIXME: If somebody feels like packaging all these things into Commands, help yourself... + log(ERROR_MESSAGE_PREFIX + Resources.getString("GameRefresher.deck_refresh_during_multiplayer")); } else { - //Drawpiles have the module definition of the Deck in the dummy child object + //Draw piles have the module definition of the Deck in the dummy child object // and a link to the actual Deck in the game. final List decksToDelete = new ArrayList<>(); final List drawPiles = getModuleDrawPiles(); @@ -387,9 +409,11 @@ public void execute(Set options, Command command) throws IllegalBuildExc int deletable = 0; int addable = 0; - log("----------"); - log(Resources.getString("GameRefresher.refreshing_decks")); - log("----------"); + if (!options.contains(SUPPRESS_INFO_REPORTS)) { + log(Resources.getString("GameRefresher.refreshing_decks")); + log(Resources.getString("GameRefresher.decks", totalDecks)); + } + for (final Map map : Map.getMapList()) { for (final GamePiece pieceOrStack : map.getPieces()) { if (pieceOrStack instanceof Deck) { @@ -417,7 +441,9 @@ public void execute(Set options, Command command) throws IllegalBuildExc foundDrawPiles.add(drawPile); final String drawPileName = drawPile.getAttributeValueString(SetupStack.NAME); - log(Resources.getString("GameRefresher.refreshing_deck", deckName, drawPileName)); + + if (!options.contains(SUPPRESS_INFO_REPORTS)) + log(Resources.getString("GameRefresher.refreshing_deck", deckName, drawPileName)); // This refreshes the existing deck with all the up-to-date drawPile fields from the module deck.removeListeners(); @@ -449,10 +475,11 @@ public void execute(Set options, Command command) throws IllegalBuildExc } } - if (options.contains("DeleteOldDecks")) { //NON-NLS + if (options.contains(DELETE_OLD_DECKS)) { //NON-NLS //log("List of Decks to remove"); for (final Deck deck : decksToDelete) { - log(Resources.getString("GameRefresher.deleting_old_deck", deck.getDeckName())); + if (!options.contains(SUPPRESS_INFO_REPORTS)) + log(Resources.getString("GameRefresher.deleting_old_deck", deck.getDeckName())); final Stack newStack = new Stack(); newStack.setMap(deck.getMap()); @@ -479,7 +506,7 @@ public void execute(Set options, Command command) throws IllegalBuildExc } } else if (!decksToDelete.isEmpty()) { - log(Resources.getString("GameRefresher.deletable_with_option")); + log(ERROR_MESSAGE_PREFIX + Resources.getString("GameRefresher.deletable_with_option")); for (final Deck deck : decksToDelete) { log(deck.getDeckName()); } @@ -508,9 +535,10 @@ else if (!decksToDelete.isEmpty()) { } if (!decksToAdd.isEmpty()) { - if (options.contains("AddNewDecks")) { //NON-NLS + if (options.contains(ADD_NEW_DECKS)) { //NON-NLS for (final DrawPile drawPile : decksToAdd) { - log(Resources.getString("GameRefresher.adding_new_deck", drawPile.getAttributeValueString(SetupStack.NAME))); + if (!options.contains(SUPPRESS_INFO_REPORTS)) + log(Resources.getString("GameRefresher.adding_new_deck", drawPile.getAttributeValueString(SetupStack.NAME))); final Deck newDeck = drawPile.makeDeck(); final Map newMap = drawPile.getMap(); @@ -525,24 +553,67 @@ else if (!decksToDelete.isEmpty()) { } } else { - log(Resources.getString("GameRefresher.addable_with_option")); + log(ERROR_MESSAGE_PREFIX + Resources.getString("GameRefresher.addable_with_option")); for (final DrawPile drawPile : decksToAdd) { log(drawPile.getAttributeValueString(SetupStack.NAME)); } } } - log("----------"); //$NON-NLS-1$ - log(Resources.getString("GameRefresher.refreshable_decks", refreshable)); - log(Resources.getString(options.contains("DeleteOldDecks") ? "GameRefresher.deletable_decks" : "GameRefresher.deletable_decks_2", deletable)); //NON-NLS - log(Resources.getString(options.contains("AddNewDecks") ? "GameRefresher.addable_decks" : "GameRefresher.addable_decks_2", addable)); //NON-NLS + // Deck reporting - anomalies & deck removes/adds are always reported + if (!options.contains(SUPPRESS_INFO_REPORTS)) + log(Resources.getString("GameRefresher.refreshable_decks", refreshable)); + + if (options.contains(DELETE_OLD_DECKS)) { + // Expecting at least 1 deletable deck and let's know how many + if (deletable == 0) { + deckWarnings++; + log(ERROR_MESSAGE_PREFIX + Resources.getString("GameRefresher.deletable_decks", 0)); + } + else log(Resources.getString("GameRefresher.deletable_decks", deletable)); + } + else { + // Expecting no deletable decks, no need to report if none found + if (deletable > 0) { + deckWarnings++; + log(ERROR_MESSAGE_PREFIX + Resources.getString("GameRefresher.deletable_decks", deletable)); + } + } + + if (options.contains(ADD_NEW_DECKS)) { + // Expecting at least 1 addable deck and let's know how many + if (addable == 0) { + deckWarnings++; + log(ERROR_MESSAGE_PREFIX + Resources.getString("GameRefresher.addable_decks", 0)); + } + else log(Resources.getString("GameRefresher.addable_decks", addable)); + } + else { + // Expecting no addable decks, no need to report if none found + if (addable > 0) { + deckWarnings++; + log(ERROR_MESSAGE_PREFIX + Resources.getString("GameRefresher.addable_decks", addable)); + } + } } } + + /* + * 5. Exit after second (final) GHK, if selected + */ + if (options.contains(USE_HOTKEY)) { //NON-NLS + // Custom finish + if (!options.contains(SUPPRESS_INFO_REPORTS)) log(Resources.getString("GameRefresher.fire_GHK", "VassalPostRefreshGHK")); + GameModule.getGameModule().fireKeyStroke(NamedKeyStroke.of("VassalPostRefreshGHK")); + } + + noGpIdMatch = gpIdChecker.getNoGpIdMatch(); // So that GpId failures accumulator can be passed back to PreDefined Setup refresher + } /** - * Before refreshing, we need to go through every piece and commemorate all the Attachment trait relationships, since they contain direct references to other GamePieces, and all of the references are + * Before refreshing, we need to go through every piece and commemorate all the Attachment trait relationships, since they contain direct references to other GamePieces, and all the references are * about to be jumbled/invalidated when new updated versions of pieces are created. * * attachmentIndex maps the old attachments (using a reference hash of the outermost piece's Unique ID plus the notionally unique Attachment Name) to the list of attachments (which are the actual old gamepieces) @@ -619,8 +690,8 @@ public void refreshAllAttachments(Command command) { * For each *new* outermost piece that has Attachments, we look up the *old* outermost piece, and use its Unique ID plus each Attachment trait's name as a lookup hash * to find the old list of contents. Then since the old list of contents is references to old versions of pieces, we use the "updatedPieces" index * to look up the new pieces and create a new set of contents for the trait. - * @param piece - * @param command + * @param piece Piece to be checked and processed + * @param command Command under construction */ public void refreshAttachment(GamePiece piece, Command command) { while (piece instanceof Decorator) { @@ -669,7 +740,9 @@ static class RefreshDialog extends JDialog { private static final long serialVersionUID = 1L; private final GameRefresher refresher; private JTextArea results; + private JCheckBox refreshPieces; private JCheckBox nameCheck; + private JCheckBox fixGPID; private JCheckBox testModeOn; private JCheckBox labelerNameCheck; private JCheckBox layerNameCheck; @@ -678,6 +751,7 @@ static class RefreshDialog extends JDialog { private JCheckBox refreshDecks; private JCheckBox deleteOldDecks; private JCheckBox addNewDecks; + private JCheckBox fireHotkey; private final Set options = new HashSet<>(); JButton runButton; @@ -699,15 +773,18 @@ public void windowClosing(WindowEvent we) { }); setLayout(new MigLayout("wrap 1", "[fill]")); //NON-NLS - final JPanel panel = new JPanel(new MigLayout("hidemode 3,wrap 1" + "," + ConfigurerLayout.STANDARD_GAPY, "[fill]")); // NON-NLS panel.setBorder(BorderFactory.createEtchedBorder()); final FlowLabel header = new FlowLabel(Resources.getString("GameRefresher.header")); + header.setFocusable(false); panel.add(header); - final JPanel buttonPanel = new JPanel(new MigLayout("ins 0", "push[]rel[]rel[]push")); // NON-NLS + // FIXME: The separator disappears if the window is resized. + final JSeparator sep = new JSeparator(JSeparator.HORIZONTAL); + panel.add(sep); + final JPanel buttonPanel = new JPanel(new MigLayout("ins 0", "push[]rel[]rel[]push")); // NON-NLS runButton = new JButton(Resources.getString("General.run")); runButton.addActionListener(e -> run()); @@ -725,38 +802,49 @@ public void windowClosing(WindowEvent we) { buttonPanel.add(exitButton, "tag cancel,sg 1"); // NON-NLS buttonPanel.add(helpButton, "tag help,sg 1"); // NON-NLS + refreshPieces = new JCheckBox(Resources.getString("GameRefresher.refresh_pieces"), true); + refreshPieces.setEnabled(false); // this is the standard default - locked as part of ensuring that at least one main option is on + panel.add(refreshPieces); + nameCheck = new JCheckBox(Resources.getString("GameRefresher.use_basic_name")); - panel.add(nameCheck); + panel.add(nameCheck, "gapx 10"); + + nameCheck.addChangeListener(e -> fixGPID.setVisible(nameCheck.isSelected())); + + fixGPID = new JCheckBox(Resources.getString("GameRefresher.fix_gpid")); + panel.add(fixGPID, "gapx 20"); + labelerNameCheck = new JCheckBox(Resources.getString("GameRefresher.use_labeler_descr"), true); - panel.add(labelerNameCheck); + panel.add(labelerNameCheck, "gapx 10"); layerNameCheck = new JCheckBox(Resources.getString("GameRefresher.use_layer_descr"), true); - panel.add(layerNameCheck); + panel.add(layerNameCheck, "gapx 10"); rotateNameCheck = new JCheckBox(Resources.getString("GameRefresher.use_rotate_descr"), true); - panel.add(rotateNameCheck); - testModeOn = new JCheckBox(Resources.getString("GameRefresher.test_mode")); - panel.add(testModeOn); - deletePieceNoMap = new JCheckBox(Resources.getString("GameRefresher.delete_piece_no_map")); - deletePieceNoMap.setSelected(true); - panel.add(deletePieceNoMap); - - refreshDecks = new JCheckBox(Resources.getString("GameRefresher.refresh_decks")); - refreshDecks.setSelected(false); - refreshDecks.addChangeListener(new ChangeListener() { - @Override - public void stateChanged(ChangeEvent e) { - deleteOldDecks.setVisible(refreshDecks.isSelected()); - addNewDecks.setVisible(refreshDecks.isSelected()); - } + panel.add(rotateNameCheck, "gapx 10"); + + deletePieceNoMap = new JCheckBox(Resources.getString("GameRefresher.delete_piece_no_map", "gapx 10"), true); + // Disabling user selection - due to issue https://github.com/vassalengine/vassal/issues/12902 + // panel.add(deletePieceNoMap); + + refreshDecks = new JCheckBox(Resources.getString("GameRefresher.refresh_decks"), false); + refreshDecks.addChangeListener(e -> { + deleteOldDecks.setVisible(refreshDecks.isSelected()); + addNewDecks.setVisible(refreshDecks.isSelected()); }); panel.add(refreshDecks); - deleteOldDecks = new JCheckBox(Resources.getString("GameRefresher.delete_old_decks")); - deleteOldDecks.setSelected(false); - panel.add(deleteOldDecks); + deleteOldDecks = new JCheckBox(Resources.getString("GameRefresher.delete_old_decks"), false); + panel.add(deleteOldDecks, "gapx 10"); - addNewDecks = new JCheckBox(Resources.getString("GameRefresher.add_new_decks")); - addNewDecks.setSelected(false); - panel.add(addNewDecks); + addNewDecks = new JCheckBox(Resources.getString("GameRefresher.add_new_decks"), false); + panel.add(addNewDecks, "gapx 10"); + + // Hotkeys setting is OFF by default to minimise risk of accidental use + fireHotkey = new JCheckBox(Resources.getString("GameRefresher.fire_global_hotkey"), false); + panel.add(fireHotkey); + + testModeOn = new JCheckBox(Resources.getString("GameRefresher.test_mode"), false); + // Disabling user selection - due to issue https://github.com/vassalengine/vassal/issues/12695 + // panel.add(testModeOn); if (refresher.isGameActive()) { refreshDecks.setSelected(false); @@ -774,29 +862,37 @@ public void stateChanged(ChangeEvent e) { SwingUtils.repack(this); + fixGPID.setVisible(nameCheck.isSelected()); + deleteOldDecks.setVisible(refreshDecks.isSelected()); addNewDecks.setVisible(refreshDecks.isSelected()); } protected void setOptions() { options.clear(); - if (nameCheck.isSelected()) { - options.add(USE_NAME); //$NON-NLS-1$ - } - if (labelerNameCheck.isSelected()) { - options.add(USE_LABELER_NAME); //$NON-NLS-1$ - } - if (layerNameCheck.isSelected()) { - options.add(USE_LAYER_NAME); //$NON-NLS-1$ - } - if (rotateNameCheck.isSelected()) { - options.add(GameRefresher.USE_ROTATE_NAME); //$NON-NLS-1$ - } - if (testModeOn.isSelected()) { - options.add(TEST_MODE); //$NON-NLS-1$ - } - if (deletePieceNoMap.isSelected()) { - options.add(DELETE_NO_MAP); //$NON-NLS-1$ + if (refreshPieces.isSelected()) { + options.add(REFRESH_PIECES); //$NON-NLS-1$ + if (nameCheck.isSelected()) { + options.add(USE_NAME); //$NON-NLS-1$ + if (fixGPID.isSelected()) { + options.add(FIX_GPID); //$NON-NLS-1$ + } + } + if (labelerNameCheck.isSelected()) { + options.add(USE_LABELER_NAME); //$NON-NLS-1$ + } + if (layerNameCheck.isSelected()) { + options.add(USE_LAYER_NAME); //$NON-NLS-1$ + } + if (rotateNameCheck.isSelected()) { + options.add(USE_ROTATE_NAME); //$NON-NLS-1$ + } + if (testModeOn.isSelected()) { + options.add(TEST_MODE); //$NON-NLS-1$ + } + if (deletePieceNoMap.isSelected()) { + options.add(DELETE_NO_MAP); //$NON-NLS-1$ + } } if (refreshDecks.isSelected()) { options.add(REFRESH_DECKS); //NON-NLS @@ -807,6 +903,9 @@ protected void setOptions() { options.add(ADD_NEW_DECKS); //NON-NLS } } + if (fireHotkey.isSelected()) { + options.add(USE_HOTKEY); //$NON-NLS-1$ + } } protected void exit() { @@ -840,7 +939,7 @@ protected void run() { // Send the update to other clients (only done in Player mode) g.sendAndLog(command); - if (options.contains("RefreshDecks") && !refresher.isGameActive()) { + if (options.contains(REFRESH_DECKS) && !refresher.isGameActive()) { final BasicLogger log = GameModule.getGameModule().getBasicLogger(); if (log != null) { log.blockUndo(1); @@ -926,7 +1025,7 @@ public List getRefreshedPieces() { * 2. Refresh the Mat * 3. Refresh each Cargo and place back on the Mat * - * @param command + * @param command Command under construction */ @Override public void refresh(Command command) { @@ -1023,7 +1122,7 @@ public void refresh(Command command) { GamePiece newPiece = gpIdChecker.createUpdatedPiece(piece); if (newPiece == null) { notFoundCount++; - log(Resources.getString("GameRefresher.refresh_error_nomatch_pieceslot", piece.getName(), piece.getId())); + log(ERROR_MESSAGE_PREFIX + Resources.getString("GameRefresher.refresh_error_nomatch_pieceslot", piece.getName(), piece.getId())); // Could not create a new piece for some reason, use the old piece newPiece = piece; } @@ -1090,7 +1189,7 @@ public void refresh(Command command) { // Create a new, updated piece if (gpIdChecker.createUpdatedPiece(piece) == null) { notFoundCount++; - log(Resources.getString("GameRefresher.refresh_error_nomatch_pieceslot", piece.getName(), piece.getId())); + log(ERROR_MESSAGE_PREFIX + Resources.getString("GameRefresher.refresh_error_nomatch_pieceslot", piece.getName(), piece.getId())); } else { updatedCount++; @@ -1118,7 +1217,7 @@ public void refresh(Command command) { newPiece = gpIdChecker.createUpdatedPiece(piece); if (newPiece == null) { notFoundCount++; - log(Resources.getString("GameRefresher.refresh_error_nomatch_pieceslot", piece.getName(), piece.getId())); + log(ERROR_MESSAGE_PREFIX + Resources.getString("GameRefresher.refresh_error_nomatch_pieceslot", piece.getName(), piece.getId())); // Could not create a new piece for some reason, use the old piece newPiece = piece; } @@ -1197,7 +1296,7 @@ public void refresh(Command command) { // Create a new, updated piece if (gpIdChecker.createUpdatedPiece(piece) == null) { notFoundCount++; - log(Resources.getString("GameRefresher.refresh_error_nomatch_pieceslot", piece.getName(), piece.getId())); + log(ERROR_MESSAGE_PREFIX + Resources.getString("GameRefresher.refresh_error_nomatch_pieceslot", piece.getName(), piece.getId())); } else { updatedCount++; @@ -1218,7 +1317,7 @@ public void refresh(Command command) { refreshedPiece = gpIdChecker.createUpdatedPiece(piece); if (refreshedPiece == null) { notFoundCount++; - log(Resources.getString("GameRefresher.refresh_error_nomatch_pieceslot", piece.getName(), piece.getId())); + log(ERROR_MESSAGE_PREFIX + Resources.getString("GameRefresher.refresh_error_nomatch_pieceslot", piece.getName(), piece.getId())); // Could not create a new piece for some reason, use the old piece refreshedPiece = piece; } diff --git a/vassal-app/src/main/java/VASSAL/build/module/PredefinedSetup.java b/vassal-app/src/main/java/VASSAL/build/module/PredefinedSetup.java index 9129529d48..ddaae04bbf 100644 --- a/vassal-app/src/main/java/VASSAL/build/module/PredefinedSetup.java +++ b/vassal-app/src/main/java/VASSAL/build/module/PredefinedSetup.java @@ -38,6 +38,7 @@ import javax.swing.AbstractAction; import javax.swing.Action; +import java.awt.Cursor; import java.awt.event.ActionEvent; import java.io.File; import java.io.IOException; @@ -90,16 +91,6 @@ public void actionPerformed(ActionEvent e) { showUseFile = () -> !isMenu; } - /* protected void setRefresherOptions() { - if (nameCheck.isSelected()) { - refresherOptions.add("useName"); - } - if (labelerNameCheck.isSelected()) { - refresherOptions.add("useLabelerName"); - } - }*/ - - @Override public String[] getAttributeDescriptions() { return new String[]{ @@ -289,8 +280,14 @@ public static String getConfigureTypeName() { return Resources.getString("Editor.PredefinedSetup.component_type"); //$NON-NLS-1$ } + @Deprecated(since = "2023-11-10", forRemoval = true) public void refresh(Set options) throws IOException, IllegalBuildException { + refreshWithStatus(options); + } + + public int refreshWithStatus(Set options) throws IOException, IllegalBuildException { if (!options.isEmpty()) { + this.refresherOptions.clear(); this.refresherOptions.addAll(options); } final GameModule mod = GameModule.getGameModule(); @@ -298,12 +295,13 @@ public void refresh(Set options) throws IOException, IllegalBuildExcepti final GameRefresher gameRefresher = new GameRefresher(mod); // since we're going to block the GUI, let's give some feedback - gameRefresher.log("----------"); //$NON-NLS-1$ - gameRefresher.log("Updating Predefined Setup: " + this.getAttributeValueString(this.NAME) + " ( " + fileName + ")"); //$NON-NLS-1$S + gameRefresher.log(GameRefresher.SEPARATOR); //$NON-NLS-1$ + gameRefresher.log("Updating Predefined Setup: " + this.getAttributeValueString(NAME) + " (" + fileName + ")"); //$NON-NLS-1$S // get a stream to the saved game in the module file gs.setupRefresh(); gs.loadGameInForeground(fileName, getSavedGameContents()); + mod.getPlayerWindow().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); // call the gameRefresher gameRefresher.execute(refresherOptions, null); @@ -319,8 +317,12 @@ public void refresh(Set options) throws IOException, IllegalBuildExcepti aw.removeFile(fileName); aw.addFile(tmpZip.getFile().getPath(), fileName); gs.closeGame(); - } + mod.getPlayerWindow().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + + // return number of refresh anomaly warnings reported + return gameRefresher.warnings(); + } @Override public HelpFile getHelpFile() { diff --git a/vassal-app/src/main/java/VASSAL/configure/RefreshPredefinedSetupsDialog.java b/vassal-app/src/main/java/VASSAL/configure/RefreshPredefinedSetupsDialog.java index c7928f0dda..80206b683e 100644 --- a/vassal-app/src/main/java/VASSAL/configure/RefreshPredefinedSetupsDialog.java +++ b/vassal-app/src/main/java/VASSAL/configure/RefreshPredefinedSetupsDialog.java @@ -20,10 +20,12 @@ import VASSAL.build.GameModule; import VASSAL.build.module.Documentation; import VASSAL.build.module.GameRefresher; +import VASSAL.build.module.GameState; import VASSAL.build.module.ModuleExtension; import VASSAL.build.module.PredefinedSetup; import VASSAL.build.module.documentation.HelpFile; import VASSAL.i18n.Resources; +import VASSAL.preferences.Prefs; import VASSAL.tools.DataArchive; import VASSAL.tools.ErrorDialog; import VASSAL.tools.swing.FlowLabel; @@ -36,24 +38,43 @@ import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JOptionPane; import javax.swing.JPanel; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; +import javax.swing.JScrollPane; +import javax.swing.JSeparator; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.ScrollPaneConstants; +import java.awt.Component; +import java.awt.Container; +import java.awt.Cursor; import java.awt.Frame; import java.awt.HeadlessException; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalTime; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import static java.time.format.DateTimeFormatter.ofPattern; +import static java.util.regex.Pattern.CASE_INSENSITIVE; public class RefreshPredefinedSetupsDialog extends JDialog { private static final Logger logger = LoggerFactory.getLogger(RefreshPredefinedSetupsDialog.class); private static final long serialVersionUID = 1L; - private JButton refreshButton; + private JCheckBox refreshPieces; private JCheckBox nameCheck; + private JCheckBox fixGPID; private JCheckBox labelerNameCheck; private JCheckBox layerNameCheck; private JCheckBox rotateNameCheck; @@ -62,35 +83,54 @@ public class RefreshPredefinedSetupsDialog extends JDialog { private JCheckBox refreshDecks; private JCheckBox deleteOldDecks; private JCheckBox addNewDecks; + private JTextField pdsFilterBox; + private String pdsFilter; + private JCheckBox reportOff; + private JCheckBox alertOn; + private boolean optionsUserMode; + private static final int FILE_NAME_REPORT_LENGTH = 24; + private JCheckBox fireHotkey; + private final Set options = new HashSet<>(); public RefreshPredefinedSetupsDialog(Frame owner) throws HeadlessException { super(owner, false); setTitle(Resources.getString("Editor.RefreshPredefinedSetupsDialog.title")); + setModal(true); initComponents(); } private void initComponents() { - setLayout(new MigLayout("", "[fill]")); // NON-NLS + optionsUserMode = true; // protects against change conflicts whilst vassal controls panel settings that are subject to stateChanged event handling - final JPanel panel = new JPanel(new MigLayout("hidemode 3,wrap 1" + "," + ConfigurerLayout.STANDARD_GAPY, "[fill]")); // NON-NLS + setLayout(new MigLayout("wrap 1", "[fill]")); // NON-NLS + + final JPanel panel = new JPanel(new MigLayout("hidemode 3,wrap 1," + ConfigurerLayout.STANDARD_GAPY, "[fill]")); // NON-NLS panel.setBorder(BorderFactory.createEtchedBorder()); final FlowLabel header = new FlowLabel(Resources.getString("GameRefresher.predefined_header")); + header.setFocusable(false); panel.add(header); + // FIXME: The separator disappears if the window is resized. + final JSeparator sep = new JSeparator(JSeparator.HORIZONTAL); + panel.add(sep); + final JPanel buttonsBox = new JPanel(new MigLayout("ins 0", "push[]rel[]rel[]push")); // NON-NLS - refreshButton = new JButton(Resources.getString("General.run")); + + + final JButton refreshButton = new JButton(Resources.getString("General.run")); refreshButton.addActionListener(e -> refreshPredefinedSetups()); - refreshButton.setEnabled(true); + //refreshButton.setEnabled(true); // this may be belt & braces - re-enabled anyway on a cancelled refresh (otherwise whole window is closed) + final JButton closeButton = new JButton(Resources.getString("General.cancel")); final JButton helpButton = new JButton(Resources.getString("General.help")); HelpFile hf = null; try { hf = new HelpFile(null, new File( - new File(Documentation.getDocumentationBaseDir(), "ReferenceManual"), - "SavedGameUpdater.html")); + new File(Documentation.getDocumentationBaseDir(), "ReferenceManual"), + "SavedGameUpdater.html")); } catch (MalformedURLException ex) { ErrorDialog.bug(ex); @@ -98,45 +138,80 @@ private void initComponents() { helpButton.addActionListener(new ShowHelpAction(hf.getContents(), null)); - final JButton closeButton = new JButton(Resources.getString("General.cancel")); closeButton.addActionListener(e -> dispose()); buttonsBox.add(refreshButton, "tag ok,sg 1"); // NON-NLS buttonsBox.add(closeButton, "tag cancel,sg 1"); // NON-NLS buttonsBox.add(helpButton, "tag help,sg 1"); // NON-NLS + refreshPieces = new JCheckBox(Resources.getString("GameRefresher.refresh_pieces"), true); + refreshPieces.setEnabled(false); // this is the standard default - locked as part of ensuring that at least one main option is on + panel.add(refreshPieces); + nameCheck = new JCheckBox(Resources.getString("GameRefresher.use_basic_name")); - panel.add(nameCheck); + + panel.add(nameCheck, "gapx 10"); + nameCheck.addChangeListener(e -> { + if (optionsUserMode) fixGPID.setVisible(nameCheck.isSelected()); + }); + + fixGPID = new JCheckBox(Resources.getString("GameRefresher.fix_gpid")); + panel.add(fixGPID, "gapx 20"); + labelerNameCheck = new JCheckBox(Resources.getString("GameRefresher.use_labeler_descr"), true); - panel.add(labelerNameCheck); + panel.add(labelerNameCheck, "gapx 10"); layerNameCheck = new JCheckBox(Resources.getString("GameRefresher.use_layer_descr"), true); - panel.add(layerNameCheck); + panel.add(layerNameCheck, "gapx 10"); rotateNameCheck = new JCheckBox(Resources.getString("GameRefresher.use_rotate_descr"), true); - panel.add(rotateNameCheck); - testModeOn = new JCheckBox(Resources.getString("GameRefresher.test_mode")); - panel.add(testModeOn); - deletePieceNoMap = new JCheckBox(Resources.getString("GameRefresher.delete_piece_no_map")); - deletePieceNoMap.setSelected(false); - panel.add(deletePieceNoMap); - - refreshDecks = new JCheckBox(Resources.getString("GameRefresher.refresh_decks")); - refreshDecks.setSelected(false); - refreshDecks.addChangeListener(new ChangeListener() { - @Override - public void stateChanged(ChangeEvent e) { + panel.add(rotateNameCheck, "gapx 10"); + + deletePieceNoMap = new JCheckBox(Resources.getString("GameRefresher.delete_piece_no_map"), true); + // Disabling user selection - due to issue https://github.com/vassalengine/vassal/issues/12902 + // panel.add(deletePieceNoMap. "gapx 10"); + + refreshDecks = new JCheckBox(Resources.getString("GameRefresher.refresh_decks"), false); + refreshDecks.addChangeListener(e -> { + if (optionsUserMode) { deleteOldDecks.setVisible(refreshDecks.isSelected()); addNewDecks.setVisible(refreshDecks.isSelected()); } }); panel.add(refreshDecks); - deleteOldDecks = new JCheckBox(Resources.getString("GameRefresher.delete_old_decks")); - deleteOldDecks.setSelected(false); - panel.add(deleteOldDecks); + deleteOldDecks = new JCheckBox(Resources.getString("GameRefresher.delete_old_decks"), false); + panel.add(deleteOldDecks, "gapx 10"); - addNewDecks = new JCheckBox(Resources.getString("GameRefresher.add_new_decks")); - addNewDecks.setSelected(false); - panel.add(addNewDecks); + addNewDecks = new JCheckBox(Resources.getString("GameRefresher.add_new_decks"), false); + panel.add(addNewDecks, "gapx 10"); + + // Post refresh hotkey setting + fireHotkey = new JCheckBox(Resources.getString("GameRefresher.fire_global_hotkey"), false); + fireHotkey.addChangeListener(e -> { + if (optionsUserMode) { + reportOff.setVisible(fireHotkey.isSelected()); + } + }); + panel.add(fireHotkey); + + reportOff = new JCheckBox(Resources.getString("Editor.RefreshPredefinedSetups.reportOff"), false); + panel.add(reportOff, "gapx 10"); + + // Separate functions that govern the overall refresh + panel.add(sep); + + testModeOn = new JCheckBox(Resources.getString("GameRefresher.test_mode"), false); + // Disabling user selection - due to issue https://github.com/vassalengine/vassal/issues/12695 + // panel.add(testModeOn); + + // PDS can be set to refresh specific items only, based on a regex + final JPanel filterPanel = new JPanel(new MigLayout(ConfigurerLayout.STANDARD_INSETS_GAPY, "[]rel[grow,fill,push]")); // NON-NLS + filterPanel.add(new JLabel(Resources.getString("Editor.RefreshPredefinedSetups.filter_prompt")), ""); + pdsFilterBox = new HintTextField(32, Resources.getString("Editor.RefreshPredefinedSetups.filter_hint")); + filterPanel.add(pdsFilterBox, "wrap"); + panel.add(filterPanel, ""); + + alertOn = new JCheckBox(Resources.getString("Editor.RefreshPredefinedSetups.alertOn"), false); + panel.add(alertOn); panel.add(buttonsBox, "grow"); // NON-NLS add(panel, "grow"); // NON-NLS @@ -147,23 +222,37 @@ public void stateChanged(ChangeEvent e) { // Default actions on Enter/ESC SwingUtils.setDefaultButtons(getRootPane(), refreshButton, closeButton); + fixGPID.setVisible(nameCheck.isSelected()); + deleteOldDecks.setVisible(refreshDecks.isSelected()); addNewDecks.setVisible(refreshDecks.isSelected()); + + reportOff.setVisible(fireHotkey.isSelected()); + + panel.setEnabled(false); } protected void setOptions() { + pdsFilter = pdsFilterBox.getText(); + options.clear(); - if (nameCheck.isSelected()) { - options.add(GameRefresher.USE_NAME); //$NON-NLS-1$ - } - if (labelerNameCheck.isSelected()) { - options.add(GameRefresher.USE_LABELER_NAME); //$NON-NLS-1$ - } - if (layerNameCheck.isSelected()) { - options.add(GameRefresher.USE_LAYER_NAME); //$NON-NLS-1$ - } - if (rotateNameCheck.isSelected()) { - options.add(GameRefresher.USE_ROTATE_NAME); //$NON-NLS-1$ + if (refreshPieces.isSelected()) { + options.add(GameRefresher.REFRESH_PIECES); //$NON-NLS-1$ + if (nameCheck.isSelected()) { + options.add(GameRefresher.USE_NAME); //$NON-NLS-1$ + if (fixGPID.isSelected()) { + options.add(GameRefresher.FIX_GPID); //$NON-NLS-1$ + } + } + if (labelerNameCheck.isSelected()) { + options.add(GameRefresher.USE_LABELER_NAME); //$NON-NLS-1$ + } + if (layerNameCheck.isSelected()) { + options.add(GameRefresher.USE_LAYER_NAME); //$NON-NLS-1$ + } + if (rotateNameCheck.isSelected()) { + options.add(GameRefresher.USE_ROTATE_NAME); //$NON-NLS-1$ + } } if (testModeOn.isSelected()) { options.add(GameRefresher.TEST_MODE); //$NON-NLS-1$ @@ -180,6 +269,12 @@ protected void setOptions() { options.add(GameRefresher.ADD_NEW_DECKS); //NON-NLS } } + if (fireHotkey.isSelected()) { + options.add(GameRefresher.USE_HOTKEY); //$NON-NLS-1$ + if (reportOff.isSelected()) { + options.add(GameRefresher.SUPPRESS_INFO_REPORTS); //$NON-NLS-1$ + } + } } public void log(String message) { @@ -188,75 +283,299 @@ public void log(String message) { } public boolean isTestMode() { - return options.contains("TestMode"); //$NON-NLS-1$ + return options.contains(GameRefresher.TEST_MODE); //$NON-NLS-1$ } - - private boolean hasAlreadyRun = false; + public boolean isFilterMode() { + return pdsFilter != null && !pdsFilter.isBlank(); //$NON-NLS-1$ + } private void refreshPredefinedSetups() { - if (hasAlreadyRun) { - return; - } - - hasAlreadyRun = true; - refreshButton.setEnabled(false); setOptions(); - if (isTestMode()) { - log(Resources.getString("GameRefresher.refresh_counters_test_mode")); + + // Disable options menu whilst the refresh is assessed (this greys the menu panel out) + optionsUserMode = false; + for (final Component component : getComponents(this)) component.setEnabled(false); + + // pre-pack regex pattern in case filter string is not found by direct string comparison + Pattern filterPattern = null; + Pattern filterPattern2 = null; + + if (isFilterMode()) { + + try { + // matching, assuming Regex with no escape of the end string modifier, otherwise match all to end + filterPattern = Pattern.compile(pdsFilter, CASE_INSENSITIVE); + filterPattern2 = Pattern.compile(pdsFilter + ".*", CASE_INSENSITIVE); + } + catch (PatternSyntaxException e) { + // something went wrong, treat regex as embedded literal + filterPattern = Pattern.compile(".*\\Q" + pdsFilter + "\\T.*", CASE_INSENSITIVE); + filterPattern2 = filterPattern; // nullify the follow-up check + log(Resources.getString("Editor.RefreshPredefinedSetups.filter_fallback")); //NON-NLS + } + pdsFilter = pdsFilter.toLowerCase(); // original search string will be used for case-insensitive string search } - // Are we running a refresh on a main module or on an extension - Boolean isRefreshOfExtension = true; + // Targeting PDS menu structure... final GameModule mod = GameModule.getGameModule(); + final GameState gs = mod.getGameState(); final DataArchive dataArchive = mod.getDataArchive(); + + // Are we running a refresh on a main module or on an extension ? final List moduleExtensionList = mod.getComponentsOf(ModuleExtension.class); - if (moduleExtensionList.isEmpty()) { - isRefreshOfExtension = false; - } + final boolean isRefreshOfExtension = !moduleExtensionList.isEmpty(); + final List modulePdsAndMenus = mod.getAllDescendantComponentsOf(PredefinedSetup.class); final List modulePds = new ArrayList<>(); + + // Error collation & reporting + final List warningPds = new ArrayList<>(); + final List warningCount = new ArrayList<>(); + final List failPds = new ArrayList<>(); + for (final PredefinedSetup pds : modulePdsAndMenus) { if (!pds.isMenu() && pds.isUseFile()) { //Exclude scenario folders (isMenu == true) // and exclude any "New game" entries (no predefined setup) (isUseFile == false) // !! Some New Game entries have UseFile = true and filename empty. Check file name too - if (pds.getFileName() != null && ! pds.getFileName().isBlank()) { - Boolean isExtensionPDS = true; + // PDS filtering option is implemented here... + final String pdsName = pds.getAttributeValueString(PredefinedSetup.NAME); + final String pdsFile = pds.getFileName(); + + if (pdsFile != null && !pdsFile.isBlank() + && (pdsFilter == null + || (pdsName != null && (pdsName.toLowerCase().contains(pdsFilter) + || (filterPattern != null && (filterPattern.matcher(pdsName).matches() || filterPattern2.matcher(pdsName).matches()))) + || (pdsFile.toLowerCase().contains(pdsFilter) + || (filterPattern != null && (filterPattern.matcher(pdsFile).matches() || filterPattern2.matcher(pdsFile).matches())))))) { + + boolean isExtensionPDS = true; + try { - isExtensionPDS = !dataArchive.contains(pds.getFileName()); + isExtensionPDS = !dataArchive.contains(pdsFile); } catch (final IOException e) { ErrorDialog.bug(e); } - if (isExtensionPDS == isRefreshOfExtension) { - modulePds.add(pds); + if (isExtensionPDS == isRefreshOfExtension) modulePds.add(pds); + } + } + } + + final int pdsCount = promptConfirmCount(modulePds); + + // check non-zero & allow an abort here whilst displaying refresh type & listing out found files + if (pdsCount > 0) { + + final Cursor oldCursor = this.getCursor(); + final Cursor waitCursor = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR); + this.setCursor(waitCursor); + + log("|" + Resources.getString("Editor.RefreshPredefinedSetupsDialog.start_refresh", mod.getGameVersion(), + isRefreshOfExtension ? " " + Resources.getString("Editor.RefreshPredefinedSetupsDialog.extension") : "")); + + // log special mode warnings to chat + if (isTestMode()) log(GameRefresher.ERROR_MESSAGE_PREFIX + Resources.getString("GameRefresher.refresh_counters_test_mode")); + if (isFilterMode()) log(GameRefresher.ERROR_MESSAGE_PREFIX + + Resources.getString("Editor.RefreshPredefinedSetups.setups_filter", ConfigureTree.noHTML(pdsFilter))); + + int i = 0; + int refreshCount = 0; + int duplicates = 0; + String lastErrorFile = null; + + final Instant startTime = Instant.now(); + final Long memoryInUseAtStart = (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / (1024*1024); + long himem = 0; + + // Process the refreshes + String hifile = null; + for (final PredefinedSetup pds : modulePds) { + + // Refresher window title updated to provide progress report + final int pct = i * 100 / pdsCount; + this.setTitle(Resources.getString("Editor.RefreshPredefinedSetupsDialog.progress", ++i, pdsCount, pct) + + (warningPds.isEmpty() ? "" : " " + + Resources.getString("Editor.RefreshPredefinedSetupsDialog.errors", lastErrorFile, warningPds.size() - 1))); + this.setCursor(waitCursor); + + final String pdsFile = pds.getFileName(); + + if (i > 1 && pdsFileProcessed(modulePds.subList(0, i - 1), pdsFile)) { + // Skip duplicate file (already refreshed) + duplicates++; + if (!options.contains(GameRefresher.SUPPRESS_INFO_REPORTS)) { + log(GameRefresher.SEPARATOR); + log(Resources.getString(Resources.getString("Editor.RefreshPredefinedSetupsDialog.skip", pds.getAttributeValueString(PredefinedSetup.NAME), pdsFile))); } } + else { + gs.setup(false); //BR// Ensure we clear any existing game data/listeners/objects out. + mod.setRefreshingSemaphore(true); //BR// Raise the semaphore that suppresses GameState.setup() + + try { + // FIXME: At this point the Refresh Options window is not responsive to Cancel, which means that runs can only be interrupted by killing the Vassal editor process + final int warnings = pds.refreshWithStatus(options); + if (warnings > 0) { + lastErrorFile = fixedLength(pdsFile, FILE_NAME_REPORT_LENGTH); + warningPds.add(pds); + warningCount.add(warnings); + } + refreshCount++; + } + catch (final IOException e) { + ErrorDialog.bug(e); + failPds.add(pds); + } + finally { + mod.setRefreshingSemaphore(false); //BR// Make sure we definitely lower the semaphore + } + } + final long mem = (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / (1024 * 1024); + if (mem > himem) { + himem = mem; + hifile = pdsFile; + } + } + + // Clean up and close the window + if (!isTestMode()) { + if (alertOn.isSelected()) { // sound alert + final SoundConfigurer c = (SoundConfigurer) Prefs.getGlobalPrefs().getOption("wakeUpSound"); + c.play(); + } + mod.setDirty(true); // ensure prompt to save when a refresh happened } + + gs.setup(false); //BR// Clear out whatever data (pieces, listeners, etc.) left over from final game loaded. + + final Duration duration = Duration.between(startTime, Instant.now()); + + log("|" + Resources.getString("Editor.RefreshPredefinedSetups.end", refreshCount)); + + if (duplicates > 0) + log(Resources.getString("Editor.RefreshPredefinedSetups.duplicates", duplicates)); + + if (!warningPds.isEmpty()) { + log(GameRefresher.ERROR_MESSAGE_PREFIX + Resources.getString("Editor.RefreshPredefinedSetups.endWarnHeading", warningPds.size())); + for (i = 0; i < warningPds.size(); i++) + log("|  " + Resources.getString("Editor.RefreshPredefinedSetups.endWarnPds", + warningPds.get(i).getAttributeValueString(PredefinedSetup.NAME), + "" + warningPds.get(i).getFileName() + "", warningCount.get(i))); + } + + if (!failPds.isEmpty()) { + log(GameRefresher.ERROR_MESSAGE_PREFIX + Resources.getString("Editor.RefreshPredefinedSetups.endFailHeading", failPds.size())); + for (i = 0; i < warningPds.size(); i++) + log("|  " + Resources.getString("Editor.RefreshPredefinedSetups.endFailPds", + failPds.get(i).getAttributeValueString(PredefinedSetup.NAME), + "" + failPds.get(i).getFileName()) + ""); + } + + log(Resources.getString("Editor.RefreshPredefinedSetups.stats", + ofPattern("HH:mm:ss").format(LocalTime.ofSecondOfDay(duration.getSeconds())), + memoryInUseAtStart, himem, hifile)); + + this.setCursor(oldCursor); + mod.getPlayerWindow().setCursor(oldCursor); } - log(modulePds.size() + " " + Resources.getString("GameRefresher.predefined_setups_found")); - for (final PredefinedSetup pds : modulePds) { - log(pds.getAttributeValueString(pds.NAME) + " (" + pds.getFileName() + ")"); + + // Exit - close window or reset for adjustments + if (pdsCount > 0 && warningPds.isEmpty() && failPds.isEmpty()) { + this.dispose(); // get rid of refresh options window on a clean run } + else { + // reset the options panel + setTitle(Resources.getString("Editor.RefreshPredefinedSetupsDialog.title")); + for (final Component component : getComponents(this)) component.setEnabled(true); + optionsUserMode = true; + } + } + private boolean pdsFileProcessed(List modulePds, String file) { for (final PredefinedSetup pds : modulePds) { - GameModule.getGameModule().getGameState().setup(false); //BR// Ensure we clear any existing game data/listeners/objects out. - GameModule.getGameModule().setRefreshingSemaphore(true); //BR// Raise the semaphore that suppresses GameState.setup() + if (pds.getFileName().equals(file)) return true; + } + return false; + } - try { - pds.refresh(options); - } - catch (final IOException e) { - ErrorDialog.bug(e); - } - finally { - GameModule.getGameModule().setRefreshingSemaphore(false); //BR// Make sure we definitely lower the semaphore + private String fixedLength(String text, int length) { + return text.length() > length ? text.substring(0, length - 3) + "..." : text; + } + + /** + * Checks the count of Pre-Defined Setups to be processed and displays appropriate message or dialog box + * + * @return number of Pre-Defined Setups to be processed. Zero if none / cancelled + */ + private int promptConfirmCount(List pdsList) { + + if (pdsList == null || pdsList.isEmpty()) { + + JOptionPane.showMessageDialog( + this, + Resources.getString("Editor.RefreshPredefinedSetups.none_found"), + Resources.getString("Editor.RefreshPredefinedSetupsDialog.title"), //$NON-NLS-1$ + JOptionPane.ERROR_MESSAGE); + + return 0; + } + + final JPanel panel = new JPanel(); + + // create the list + final JTextArea display = new JTextArea(16, 60); + display.setEditable(false); // set textArea non-editable + final JScrollPane scroll = new JScrollPane(display, + ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, + ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); + panel.add(scroll); + + int i = 0; + + for (final PredefinedSetup pds : pdsList) + + // tab separation to make it easier to re-use the data (e.g. copy to spreadsheet) + display.append((i++ > 0 ? System.lineSeparator() : "") + pds.getAttributeValueString(PredefinedSetup.NAME) + + Character.toString(9) + pds.getFileName()); + + // return number of PDS items or zero if refresh is cancelled + return JOptionPane.showConfirmDialog( + this, + panel, + Resources.getString("Editor.RefreshPredefinedSetups.confirm_title", + Resources.getString(isTestMode() ? "Editor.RefreshPredefinedSetups.confirm.test" : "Editor.RefreshPredefinedSetups.confirm.run"), + i, isFilterMode() ? " " + Resources.getString("Editor.RefreshPredefinedSetups.confirm.filter") : ""), //$NON-NLS-1$ + JOptionPane.OK_CANCEL_OPTION, + JOptionPane.QUESTION_MESSAGE) == JOptionPane.OK_OPTION ? i : 0; + } + + /** + * Recursively collate all non-Container components inside a Container + * Ref: ... + * + * @param container Container e.g. a JPanel + * + * @return Non-Container contents + */ + private Component[] getComponents(Component container) { + ArrayList list; + + try { + list = new ArrayList<>(Arrays.asList( + ((Container) container).getComponents())); + for (int index = 0; index < list.size(); index++) { + Collections.addAll(list, getComponents(list.get(index))); } } - GameModule.getGameModule().getGameState().setup(false); //BR// Clear out whatever data (pieces, listeners, etc) left over from final game loaded. + catch (ClassCastException e) { + list = new ArrayList<>(); + } - refreshButton.setEnabled(true); + return list.toArray(new Component[0]); } + + } diff --git a/vassal-app/src/main/resources/VASSAL/i18n/Editor.properties b/vassal-app/src/main/resources/VASSAL/i18n/Editor.properties index acb31a6dd6..15b3b055f6 100644 --- a/vassal-app/src/main/resources/VASSAL/i18n/Editor.properties +++ b/vassal-app/src/main/resources/VASSAL/i18n/Editor.properties @@ -2044,6 +2044,29 @@ Editor.ReturnToDeck.deck_name=Deck Name # Refresh Predefined Setups Dialog Editor.ModuleEditor.refresh_predefined=Refresh Predefined Setups Editor.RefreshPredefinedSetupsDialog.title=Refresh Predefined Setups +Editor.RefreshPredefinedSetups.filter_hint=Literal or Regular Expression (blank for all) +Editor.RefreshPredefinedSetups.filter_prompt=Limit to titles & files matching: +Editor.RefreshPredefinedSetupsDialog.start_refresh=Refreshing Predefined Setups with Module version %1$s +Editor.RefreshPredefinedSetupsDialog.extension=(extension predefined setups) +Editor.RefreshPredefinedSetups.setups_filter=Predefined setups filtered on "%1$s" +Editor.RefreshPredefinedSetups.filter_fallback=Bad regular expression, falling back on literal search +Editor.RefreshPredefinedSetupsDialog.progress=Refresh Predefined Setups: %3$s%% (%1$s of %2$s) +Editor.RefreshPredefinedSetupsDialog.errors=Warnings: %1$s (+%2$s more) +Editor.RefreshPredefinedSetupsDialog.skip=Skipping Predefined Setup: %1$s (%2$s) - file already refreshed +Editor.RefreshPredefinedSetups.reportOff=Suppress refresh information reports +Editor.RefreshPredefinedSetups.alertOn=Play wake-up alert when done +Editor.RefreshPredefinedSetups.none_found=No Predefined Setups found +Editor.RefreshPredefinedSetups.confirm_title=%1$s Refresh %2$s Predefined Setups%3$s +Editor.RefreshPredefinedSetups.confirm.run=Confirm +Editor.RefreshPredefinedSetups.confirm.test=Test +Editor.RefreshPredefinedSetups.confirm.filter=(filtered) +Editor.RefreshPredefinedSetups.end=*** Predefined Setups refresh complete: %1$s game files refreshed *** +Editor.RefreshPredefinedSetups.duplicates=Skipped %1$s duplicate game files +Editor.RefreshPredefinedSetups.endWarnHeading=%1$s files refreshed with warnings: +Editor.RefreshPredefinedSetups.endWarnPds=%3$s warnings from %2$s (setup: %1$s) +Editor.RefreshPredefinedSetups.endFailHeading==%1$s files not refreshed due to access failure: +Editor.RefreshPredefinedSetups.endFailPds=%2$s (setup: %1$s) +Editor.RefreshPredefinedSetups.stats=Elapsed time: %1$s / Memory usage start: %2$s Mb, high: %3$s Mb (file: %4$s) # Saved Game Updater # 1 - a Game Piece diff --git a/vassal-app/src/main/resources/VASSAL/i18n/VASSAL.properties b/vassal-app/src/main/resources/VASSAL/i18n/VASSAL.properties index fd21c258f8..6f170f2a31 100644 --- a/vassal-app/src/main/resources/VASSAL/i18n/VASSAL.properties +++ b/vassal-app/src/main/resources/VASSAL/i18n/VASSAL.properties @@ -683,15 +683,17 @@ GameModule.save_module=Save Module? GameRefresher.refresh_counters=Refresh Counters GameRefresher.game_started_in_editor=Refresh of predefined setups not possible when a game is open. Close the open game in the Player window and retry. GameRefresher.game_is_replaying=Refresh Counters is not allowed while a log is being replayed. Counters can be refreshed once the log has been fully replayed. +GameRefresher.refresh_pieces=Refresh piece definitions with latest settings from module -GameRefresher.get_all_pieces=Collecting Counters -GameRefresher.counters_total=%1$s Counters found in game GameRefresher.counters_kept=- %1$s Counters collected -GameRefresher.counters_not_owned=- %1$s Counters not collected - Not owned -GameRefresher.counters_not_visible=- %1$s Counters not collected - Not visible +GameRefresher.decks=- %1$s Decks identified +GameRefresher.counters_not_owned=- %1$s Counters not collected - not owned +GameRefresher.counters_not_visible=- %1$s Counters not collected - not visible GameRefresher.run_refresh_counters_v2=%1$s Refreshing Counters with Module version %2$s +GameRefresher.fire_GHK=ACTIVATING GLOBAL HOTKEY %1$s GameRefresher.run_refresh_counters_v3=Refreshing Counters with Module version %1$s +GameRefresher.run_refresh_counters_v4=REFRESHING COUNTERS GameRefresher.counters_refreshed=- %1$s Counters refreshed GameRefresher.counters_not_found=- %1$s Counters could not be refreshed - Not found GameRefresher.counters_no_map=- %1$s Counters could not be refreshed - Not on a map @@ -701,10 +703,12 @@ GameRefresher.refresh_counters_test_mode=Refresh Counters - Test Mode - Game wil GameRefresher.delete_piece_no_map=Delete pieces without a map GameRefresher.test_mode=Test Mode - Game will not be updated GameRefresher.use_basic_name=Use counter names to identify unknown counters +GameRefresher.fire_global_hotkey=After refresh trigger Global Hotkey VassalPostRefreshGHK +GameRefresher.fix_gpid=Refreshed counter will adopt matching counter's Piece Id GameRefresher.use_labeler_descr=Use Label descriptions to match modified Text Label traits GameRefresher.use_layer_descr=Use Layer names to match modified Layer traits GameRefresher.use_rotate_descr=Use Rotator names to match modified Can Rotate traits -GameRefresher.predefined_setups_found=Predefined setups found +GameRefresher.predefined_setups_found=predefined setups found GameRefresher.gpid_error_message=Unable to run Refresh, module was saved with older vassal version. Edit and save module with latest vassal version first. GameRefresher.refresh_error_nomap1=Cannot refresh piece %1$s (%2$s): No Map GameRefresher.refresh_error_nomap2=Deleting %1$s (%2$s) from game @@ -712,8 +716,8 @@ GameRefresher.refresh_error_nostack=Warning: Piece %1$s (%2$s) is not linked to GameRefresher.refresh_error_nostackindex=Warning: Piece %1$s (%2$s) is missing position in stack. Will default to 1 GameRefresher.refresh_error_nomatch_pieceslot=Cannot refresh piece %1$s (%2$s): Can't find matching Piece Slot -GameRefresher.header=Game Refresher: If you are using a more recent version of the module than this game was created with, this tool will update the pieces and decks in the game to use the latest up-to-date module prototypes & settings. See HELP for additional information. -GameRefresher.predefined_header=Refresh Predefined Setups: This tool updates the piece definitions and decks in all Pre-defined Setups to match the most up-to-date prototype definitions and deck settings. See HELP for additional information. +GameRefresher.header=Game Refresher: Use this tool to update the current game if compatible with the module or extension. See HELP before using this tool for the first time. +GameRefresher.predefined_header=Refresh Predefined Setups: Use this tool to update Predefined Setups in a module or extension. See HELP for additional information. GameRefresher.deck_refresh_during_multiplayer=*** Decks cannot be refreshed while a logfile is being created or an online connection exists. *** GameRefresher.refreshing_decks=REFRESHING DECKS GameRefresher.refreshing_deck=Refreshing Deck: %1$s with properties from module deck %2$s @@ -817,6 +821,11 @@ GpIdChecker.piece_gpid_updated=%1$s GPID updated from %2$s to %3$s GpIdChecker.piece_slot=Piece Slot %1$s # Names a Trait within the piece - either a "Place Marker" or a "Replace With Other" Trait. GpIdChecker.place_replace_trait=Place/Replace Trait %1$s +# A piece being matched by name having failed to match on slot +GpIdChecker.refreshByName=Piece "%1$s" (GPID %2$s) matched on name and refreshed from GPID %3$s +GpIdChecker.fixGPID=(GPID updated!) +GpIdChecker.refreshByNameFail=Unable to match piece "%1$s" (GPID %2$s) by name +GpIdChecker.SlotNotFound=Slot not found for Piece "%1$s" (GPID %2$s); consider refresh option Use counter names... # Help Window Help.error_log=Show Error Log diff --git a/vassal-doc/src/main/readme-referencemanual/ReferenceManual/GameRefresher.adoc b/vassal-doc/src/main/readme-referencemanual/ReferenceManual/GameRefresher.adoc index 3e3c115f22..1c4be81a13 100644 --- a/vassal-doc/src/main/readme-referencemanual/ReferenceManual/GameRefresher.adoc +++ b/vassal-doc/src/main/readme-referencemanual/ReferenceManual/GameRefresher.adoc @@ -6,62 +6,68 @@ ''''' === Refresh Counters -When you update the <> and <> in a module, those changes will affect any *future* games started using that module, but the changes will not--by default at least--affect pieces in any ongoing games that you load with the new version of the module. VASSAL saved games include the complete definition of each piece in order to maintain saved game compatibility with older versions of a module: so that replays and saves sent to you by someone with an earlier version of the module will continue to work in the same way they always did with the old version. +When you update the <> in a module, or <> that are applied to them, those changes will affect any *future* games started using that module, but the changes will not--by default at least--affect pieces in any ongoing games that you load with the new version of the module. The same is true of decks and stacks. VASSAL saved games include the complete definition of these components in order to maintain saved game compatibility with older versions of a module: so that replays and saves sent to you by someone with an earlier version of the module will continue to work in the same way they always did with the old version. -But particularly since the <> for module scenarios are stored internally as saved games, it is often important to module designers to be able to update an existing game to use the latest prototypes. That way a module designer can often avoid re-doing complex setups simply because prototypes have been updated and improved. +Since the <> for module scenarios are stored internally as saved games, it is often important to module designers to be able to update an existing game to use the latest prototypes. That way a module designer can often avoid re-doing complex setups simply because prototypes have been updated and improved. -To use the refresher on the currently loaded game, go to the _Tools_ menu in your main VASSAL window and select _Refresh Counters_. You will be shown a dialog with several choices affecting the manner in which the operation is to be carried out. +To use the refresher on the currently loaded game, go to the _Tools_ menu in your main VASSAL window and select _Refresh Counters_. You will be shown a dialog with several choices affecting the manner in which the operation is to be carried out. The screenshot below shows all main options selected in order to reveal the sub-options. Normally, the Refresher's standard mode is to refresh pieces only, without trying to identify unknown counters. Click the _Run_ button when you are ready to perform the refresh. The chat log will show output and statistics from the operation. Once the operation is finished, pieces in the game (as well as new pieces created from <>) will make use of the most recent prototypes. Whenever a piece is created in a VASSAL game, the Id of the definition used to create it is saved in the Piece. This Id identifies a piece in a <>, or a Piece Definition in a <> or <> trait. -The Game Refresher works by matching the Id in each piece in the current game to the Id's of all piece definitions in the current module to find the new definition. If a match is found, then the piece is replaced with one created from the new defintion. Then each trait in the new Piece is checked to see if there is an EXACTLY matching trait in the old definition. If an EXACT match is found, then the 'state' of the old trait is copied over (e.g. what is the current layer showing, or current rotation facing). +The Game Refresher works by matching the Id in each piece in the current game to the Id's of all piece definitions in the current module to find the new definition. If a match is found, then the piece is replaced with one created from the new definition. Then each trait in the new Piece is checked to see if there is an EXACTLY matching trait in the old definition. If an EXACT match is found, then the 'state' of the old trait is copied over (e.g. what is the current layer showing, or current rotation facing). -Problems occur when the definition used to create the piece no longer exists in the module, or if traits are modified slightly so that they no longer EXACTLY match the old piece. There are various options in the Game Refresher dialog that can be used to help match and update these pieces and traits. +Problems occur when the definition used to create the piece no longer exists in the module, or if traits are modified slightly so that they no longer EXACTLY match the old piece. There are various options in the Game Refresher dialog that can be used to help match and update these pieces and traits. In addition, there is an option to trigger a <<#RefreshHotkey,special hotkey>> to perform custom maintenance routines which you have designed. [.text-center] image:images/GameRefresher.png[] [width="100%",cols="50%a",] |=== -| *Use counter names to identify unkown counters:*:: -Use this option when the piece defintion used to create a Game Piece no longer exists in the module. + +|*Refresh piece definitions with latest settings from module:*:: + +This feature is mandatory and will refresh <> in the game with the following sub-options: + +*- Use counter names to identify unknown counters:*:: +Use this option when the piece definition used to create a Game Piece no longer exists in the module. + + This option tells the Refresher to find a Piece Definition with the same Basic Name as the old definition to use for the Refresh. The first such definition found will be used. +- *Refreshed counter will adopt matching counter's Piece Id:*:: + +This sub-option is available when "Use counter names..." is selected. The counter will be repaired using the matching counter's Piece Id ("GPID"). ⚠️ Exercise prudence when considering this option, to be sure that the matching counter is correct. + *Use Label descriptions to match modified Text Label traits:*:: If you change any of the options in a <> trait, then the new definition will not exactly match the old counter and the current setting of the *Text Label* will revert to the default recorded in the new definition. + + This option tells the Refresher to find a *Text Label* trait in the new definition that has the same *Description* as in the old definition. This allows the current value of the *Text Label* to be carried across to the piece, even if the *Text Label* trait has been modified as long as the Description field has not been changed. -*Use Layer names to match modified Layer traits:*:: +*- Use Layer names to match modified Layer traits:*:: If you change any of the options in a <> trait, then the new definition will not exactly match the old counter and the current Activation and Layer level will revert to the default recorded in the new definition. + + This option tells the Refresher to find a *Layer* trait in the new definition that has the same *Name* as in the old definition. This allows the current activation status and Layer level to be carried across to the piece, even if the *Layer* trait has been modified as long as the *Name* field has not been changed. -*Use Rotator names to match modified Can Rotate traits:*:: +*- Use Rotator names to match modified Can Rotate traits:*:: If you change any of the options in a <> trait, then the new definition will not exactly match the old counter and the current rotation angle will revert to the default recorded in the new definition. + + This option tells the Refresher to find a *Can Rotate* trait in the new definition that has the same *Rotator Name* as in the old definition. This allows the current rotation angle to be carried across to the piece, even if the *Can Rotate* trait has been modified as long as the *Rotator Name* field has not been changed. -*Refresh Counters - Test Mode - Game will not be updated:*:: -Runs a full refresh and generates a report on how many counters are refreshed and how many could not be refreshed using the current options, but does NOT make any changes to the module. - -*Delete pieces without a map:*:: -Remove any pieces found in the game that do not exist on a Map. These pieces would be inaccessible to players and are a result of Vassal bugs. This option cleans these pieces up and helps reduce the size of a saved game file. *Refresh decks' properties with latest settings from module:*:: -Allow <> in the game to be refreshed. See the <<#DeckRefresher,Deck Refresher>> section below for full details. +Allow <> in the game to be refreshed, with the following two sub-options. See the <<#DeckRefresher,Deck Refresher>> section below for full details. -*Delete decks which no longer exist in the module (any contents will be left on map in a stack):*:: +*- Delete decks which no longer exist in the module (any contents will be left on map in a stack):*:: Remove Decks from the game that no longer exist in the module. See the <<#DeckRefresher,Deck Refresher>> section below for full details. -*Add decks to game which have been added to the module since this game was created (empty deck will be added):*:: +*- Add decks to game which have been added to the module since this game was created (empty deck will be added):*:: -Add Decks to teh game that have been added to the module. See the <<#DeckRefresher,Deck Refresher>> section below for full details. +Add Decks to the game that have been added to the module. See the <<#DeckRefresher,Deck Refresher>> section below for full details. + +*After refresh trigger Global Hotkey _VassalPostRefreshGHK_:*:: +Use this option to invoke a special Global Hotkey, _VassalPostRefreshGHK_, which will execute as a final refresh step. See the <<#RefreshHotkey, Post-Refresh Hotkey>> section below for more details. |=== @@ -70,7 +76,7 @@ You can then save the game, or simply continue playing it from that point. [#DeckRefresher] ==== Deck Refresher -As of VASSAL 3.6 you can also refresh the <> in a module. Like Game Pieces, Decks are not normally updated from the module definitions _during_ a game, and so if you have an updated version of the module and load a saved game the deck will still behave according to the original settings. This maintains backward-compatibility with saves and logs made with earlier versions of a module, but it can become awkward when managing modules that use <> as starting positions. The Deck Refresher lets you update, add, and delete decks in a game, for this reason. +As of VASSAL 3.6 you can also refresh the <> in a module. Like Game Pieces, Decks are not normally updated from the module definitions _during_ a game, and so if you have an updated version of the module and load a saved game the deck will still behave according to the original settings. This maintains backward-compatibility with saves and logs made with earlier versions of a module, but it can become awkward when managing modules that use <> as starting positions. The Deck Refresher lets you update, add, and delete decks in a game, for this reason. If you select the _Refresh decks_ option when running the Game Refresher, existing decks will be refreshed from the latest settings and positions in the module definition. This will update almost all the properties of the deck, including key commands, menu text, and the various check-box options that configure a deck. A deck can even be moved from one position to another this way. However, decks are matched by name, so changing the _name_ of a deck will make the deck refresher think that a deck of the old name has been deleted and a new deck has been created. @@ -81,3 +87,14 @@ If you select the _Delete decks_ option, then any deck found in the current game If you select the _Add decks_ option, then any _new_ deck found in the module definition that does not exist in the game being refreshed will be _added_. Note this will not add any _contents_ (e.g., cards) to the deck, it will only add the deck. If you need to add contents you will need to arrange to add them separately, e.g., from a piece palette, or dragged in from some other location. +[#RefreshHotkey] +==== Post-Refresh Hotkey +When the hotkey option is checked, the Refresher will trigger the special hotkey _VassalPostRefreshGHK_ after refreshing. The module developer can use this feature to perform additional maintenance on predefined setup files or to facilitate upgrading of an externally loaded game. Potential uses include converting counters or populating a new deck. + +This options can be used to extend the automated functionality of the game refresher, especially when running via the _Refresh Pre-Defined Setups_ tool outlined in <>. + +Design and test your maintenance actions carefully. You can use _Refresh Counters_ to do one-off tests. Also, remember that Startup GKCs are not executed during _Refresh Predefined Setups_. + +After using _Refresh Predefined Setups_, save your module as a different file name so you can do re-runs on the original if need be. + +Once you are done, consider disabling or removing the maintenance components so that further refreshes don’t trigger them accidentally. \ No newline at end of file diff --git a/vassal-doc/src/main/readme-referencemanual/ReferenceManual/SavedGameUpdater.adoc b/vassal-doc/src/main/readme-referencemanual/ReferenceManual/SavedGameUpdater.adoc index d976ad6750..96f3f48e00 100644 --- a/vassal-doc/src/main/readme-referencemanual/ReferenceManual/SavedGameUpdater.adoc +++ b/vassal-doc/src/main/readme-referencemanual/ReferenceManual/SavedGameUpdater.adoc @@ -6,17 +6,19 @@ ''''' === Refresh Counters -When you update the <> and <> in a module, those changes will affect any *future* games started using that module, but the changes will not--by default at least--affect pieces in any ongoing games that you load with the new version of the module. VASSAL saved games include the complete definition of each piece in order to maintain saved game compatibility with older versions of a module: so that replays and saves sent to you by someone with an earlier version of the module will continue to work in the same way they always did with the old version. +When you update the <>, <> and <> in a module, those changes will affect any *future* games started using that module but the changes will not, by default at least, affect pieces in any ongoing games that you load with the new version of the module. VASSAL saved games include the complete definition of each piece in order to maintain saved game compatibility with older versions of a module: so that replays and saves sent to you by someone with an earlier version of the module will continue to work in the same way they always did with the old version. -But particularly since the <> for module scenarios are stored internally as saved games, it is often important to module designers to be able to update an existing game to use the latest prototypes. That way a module designer can often avoid re-doing complex setups simply because prototypes have been updated and improved. +Since <> for module scenarios are stored internally as saved games, it is often important to module designers to be able to update an existing game to use the latest piece, prototype and deck definitions. That way a module designer can often avoid re-doing complex setups simply because pieces or decks have been updated and improved. -Running the _Refresh Predefined Setups_ tool is equivalent to running the <> on each of the predefined setups in a module. Please see that page for detail information on the Refresher options. +To use the refresher on the <> in a module go to the Editor's _Tools_ menu and select _Refresh Predefined Setups_. You will be shown a dialog with several choices affecting the manner in which the operation is to be carried out. -To use the refresher on all the <> in a module go to the _Editor's_ _Tools_ menu and select _Refresh Pre-Defined Setups_. You will be shown a dialog with several choices affecting the manner in which the operation is to be carried out. +The _Refresh Predefined Setups_ tool applies the <> tool to the predefined setups in a module, with the important caveat that Startup GKCs are turned off. +The _Refresh Predefined Setups_ tool offers all the options that are described for <>. In addition, further options offer a sound alert at the end of the refresh run and the ability to filter the predefined setup files to be included. Use this field to limit the refresh whilst testing a module during development; leave blank to refresh all predefined setups. + +When the post-refresh Global Hotkey is selected (see screenshot), the tool offers a further option to suppress (most) refresh reporting. This will make the post-refresh Global Hotkey more useful as a trigger for custom reporting on predefined setups. Warnings and critical refresh events (Deck adds/deletes etc) are still reported when this option is selected. [.text-center] image:images/SavedGameUpdater.png[] -Click the _Run_ button when you are ready to perform the refresh. _All_ Predefined Setups in the module will be updated, _which may take some time, especially for complex modules with many pre-defined setups._ The chat log will show output and statistics from the operation. Once the operation is finished, pieces in all pre-defined setups will have been updated to use the latest prototypes. -Note that you should then save your module to complete the update. +Click the _Run_ button when you are ready to perform the refresh. A confirmation box will be displayed, including a list of all the scenario names found and their associated files to be refreshed. Cancel to go back to the previous step or confirm to start updating the predefined setups selected. Update may take some time, especially for complex modules with many predefined setups. During the refresh, a progress status is displayed in the title bar of the options window. Once the refresh run is finished, pieces in all predefined setups will have been updated to use the latest traits. The chat log will show output and statistics from the operation. You will need to save the module to complete the update. diff --git a/vassal-doc/src/main/readme-referencemanual/ReferenceManual/images/GameRefresher.png b/vassal-doc/src/main/readme-referencemanual/ReferenceManual/images/GameRefresher.png index 859bcd115d..96cd1f1e3d 100644 Binary files a/vassal-doc/src/main/readme-referencemanual/ReferenceManual/images/GameRefresher.png and b/vassal-doc/src/main/readme-referencemanual/ReferenceManual/images/GameRefresher.png differ diff --git a/vassal-doc/src/main/readme-referencemanual/ReferenceManual/images/SavedGameUpdater.png b/vassal-doc/src/main/readme-referencemanual/ReferenceManual/images/SavedGameUpdater.png index 973817b128..f439b740c5 100644 Binary files a/vassal-doc/src/main/readme-referencemanual/ReferenceManual/images/SavedGameUpdater.png and b/vassal-doc/src/main/readme-referencemanual/ReferenceManual/images/SavedGameUpdater.png differ