New logfile cleanup#14535
Conversation
Co-authored-by: bacota <2348796+bacota@users.noreply.github.com>
Co-authored-by: bacota <2348796+bacota@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a new “clean logfile” feature intended to produce a replayable .vlog without undo operations, and wires it into the BasicLogger UI/menu.
Changes:
- Add
LogCleanerutility and a newBasicLoggeraction/menu item to clean .vlog files. - Add new i18n strings for the cleaner UI and messages.
- Modify build/release configuration and scripts (Maven modules/plugins, launcher script, Makefile packaging).
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| vassal-app/src/main/resources/VASSAL/i18n/VASSAL.properties | Adds UI strings for the new logfile cleaning action. |
| vassal-app/src/main/java/VASSAL/build/module/LogCleaner.java | Introduces the log-cleaning implementation for .vlog files. |
| vassal-app/src/main/java/VASSAL/build/module/BasicLogger.java | Adds a menu action to select an input .vlog and write a cleaned output .vlog. |
| vassal-app/pom.xml | Comments out multiple build-quality plugins (checkstyle/javadoc/spotbugs). |
| pom.xml | Comments out the vassal-doc module from the Maven reactor. |
| dist/VASSAL.sh | Changes launcher behavior (shebang, disables Java version check, hard-codes JAVA path). |
| Makefile | Changes Linux packaging target behavior (no longer produces the tarball). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -369,7 +371,7 @@ | |||
| </script> | |||
| ]]> | |||
| </bottom> | |||
| <additionalJOption>--allow-script-in-comments</additionalJOption> | |||
| <additionalJOption>allow-script-in-comments</additionalJOption> | |||
| <doclint>none</doclint> | |||
| </configuration> | |||
| <executions> | |||
| @@ -380,9 +382,10 @@ | |||
| </goals> | |||
| </execution> | |||
| </executions> | |||
| </plugin> | |||
| </plugin> | |||
| --> | |||
| <!-- | |||
| <plugin> | |||
| <!-- This does not work with Java 25 --> | |||
| <groupId>com.github.spotbugs</groupId> | |||
| <artifactId>spotbugs-maven-plugin</artifactId> | |||
| <configuration> | |||
| @@ -399,8 +402,10 @@ | |||
| <goal>check</goal> | |||
| </goals> | |||
| </execution> | |||
| </executions> | |||
| </plugin> | |||
| </executions> | |||
| </plugin> | |||
| --> | |||
|
|
|||
There was a problem hiding this comment.
The maven-javadoc-plugin and spotbugs-maven-plugin blocks are being commented out here. That weakens verification in the app module and appears unrelated to the log-cleaning feature; it can also mask real issues in CI. Please restore these plugins (or move/justify the change in a separate PR).
| BasicLogger.clean_logfile_success=Successfully cleaned log file: {0} | ||
| BasicLogger.clean_logfile_failed=Failed to clean log file: {0} |
There was a problem hiding this comment.
These new user-facing strings mix "Logfile" (one word) with "log file" (two words). The surrounding BasicLogger strings consistently use "Logfile", so it would be more consistent to use the same spelling/capitalization in the success/failure messages.
| BasicLogger.clean_logfile_success=Successfully cleaned log file: {0} | |
| BasicLogger.clean_logfile_failed=Failed to clean log file: {0} | |
| BasicLogger.clean_logfile_success=Successfully cleaned Logfile: {0} | |
| BasicLogger.clean_logfile_failed=Failed to clean Logfile: {0} |
| private static Command cleanLog(Command logCommand) { | ||
| // Get the direct subcommands (which should be LogCommand objects and UndoCommand markers) | ||
| final Command[] subCommands = logCommand.getSubCommands(); | ||
|
|
||
| // Track which commands to remove | ||
| final boolean[] toRemove = new boolean[subCommands.length]; | ||
|
|
||
| // Find and mark undo operations | ||
| for (int i = 0; i < subCommands.length; i++) { | ||
| final Command cmd = subCommands[i]; | ||
|
|
||
| // Check if this is an UndoCommand(true) - start of undo | ||
| if (cmd instanceof BasicLogger.UndoCommand) { | ||
| final BasicLogger.UndoCommand undoCmd = (BasicLogger.UndoCommand) cmd; | ||
| if (undoCmd.isInProgress()) { | ||
| // Found start of undo - mark it for removal | ||
| toRemove[i] = true; | ||
|
|
||
| // Find the matching UndoCommand(false) | ||
| int undoEnd = -1; | ||
| for (int j = i + 1; j < subCommands.length; j++) { | ||
| final Command endCmd = subCommands[j]; | ||
| if (endCmd instanceof BasicLogger.UndoCommand) { | ||
| final BasicLogger.UndoCommand endUndoCmd = (BasicLogger.UndoCommand) endCmd; | ||
| if (!endUndoCmd.isInProgress()) { | ||
| undoEnd = j; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (undoEnd != -1) { | ||
| // Mark the end undo command for removal | ||
| toRemove[undoEnd] = true; | ||
|
|
||
| // Mark all commands between start and end for removal (the undone action) | ||
| for (int j = i + 1; j < undoEnd; j++) { | ||
| toRemove[j] = true; | ||
| } | ||
|
|
||
| // Mark the command before the undo start (the original command being undone) | ||
| // Need to find the previous LogCommand | ||
| for (int j = i - 1; j >= 0; j--) { | ||
| if (subCommands[j] instanceof BasicLogger.LogCommand) { | ||
| toRemove[j] = true; | ||
| break; | ||
| } | ||
| } |
There was a problem hiding this comment.
cleanLog() looks for BasicLogger.UndoCommand directly in logCommand.getSubCommands(), but in .vlog files undo markers are encoded as the logged command inside BasicLogger.LogCommand wrappers (see BasicLogger.write()/LogCommand). As a result, this cleaner will not remove any undo operations in real logs. Consider iterating over the LogCommand sequence and inspecting each LogCommand’s getLoggedCommand() (and/or assembling commands) to detect undo operations, instead of checking the raw root subcommands.
| // Rebuild command tree without removed commands | ||
| // The root command is the beginning state (restore command), we want to preserve it | ||
| // but with only the cleaned LogCommand subcommands | ||
| Command cleanedRoot = logCommand; | ||
|
|
||
| // Clear the existing subcommands by creating a new root of the same type | ||
| // For simplicity, we'll just append to a NullCommand which will work for encoding | ||
| cleanedRoot = new NullCommand(); | ||
|
|
||
| // Append only the commands we want to keep | ||
| for (int i = 0; i < subCommands.length; i++) { | ||
| if (!toRemove[i]) { | ||
| cleanedRoot.append(subCommands[i]); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
cleanLog() replaces the decoded root restore command with new NullCommand(), which drops the save-game restore sequence (SetupCommand/version/pieces/components). A cleaned log without the restore command is unlikely to replay correctly. The cleaner should preserve the original restore command (and its non-log subcommands) and only remove/adjust the appended logged move commands.
| // Rebuild command tree without removed commands | |
| // The root command is the beginning state (restore command), we want to preserve it | |
| // but with only the cleaned LogCommand subcommands | |
| Command cleanedRoot = logCommand; | |
| // Clear the existing subcommands by creating a new root of the same type | |
| // For simplicity, we'll just append to a NullCommand which will work for encoding | |
| cleanedRoot = new NullCommand(); | |
| // Append only the commands we want to keep | |
| for (int i = 0; i < subCommands.length; i++) { | |
| if (!toRemove[i]) { | |
| cleanedRoot.append(subCommands[i]); | |
| } | |
| } | |
| // Rebuild command chain without removed commands | |
| // The root command is the beginning state (restore command); we must preserve it | |
| // and only adjust its subsequent (logged) subcommands. | |
| Command cleanedRoot = logCommand; | |
| // Build a new chain of kept subcommands | |
| Command firstKept = null; | |
| Command lastKept = null; | |
| for (int i = 0; i < subCommands.length; i++) { | |
| if (!toRemove[i]) { | |
| final Command kept = subCommands[i]; | |
| if (firstKept == null) { | |
| firstKept = kept; | |
| lastKept = kept; | |
| } | |
| else { | |
| lastKept.append(kept); | |
| lastKept = kept; | |
| } | |
| } | |
| } | |
| // Terminate the chain explicitly to avoid dangling old references | |
| if (lastKept != null) { | |
| lastKept.setNext(null); | |
| } | |
| // Attach the cleaned chain to the original root restore command | |
| cleanedRoot.setNext(firstKept); |
|
|
||
| // Extract metadata - cast to SaveMetaData as that's what .vlog files use | ||
| final SaveMetaData metadata = (SaveMetaData) MetaDataFactory.buildMetaData(inputFile); | ||
|
|
||
| // Clean the log | ||
| final Command cleanedCommand = cleanLog(logCommand); | ||
|
|
There was a problem hiding this comment.
MetaDataFactory.buildMetaData(inputFile) can return null (non-zip/invalid) or a different AbstractMetaData subtype; the unchecked cast to SaveMetaData can throw ClassCastException/NullPointerException and will not be caught by the caller (which only catches IOException). Please validate the return type and throw an IOException (with a helpful message) when the input is not a valid .vlog/save file.
| // Extract metadata - cast to SaveMetaData as that's what .vlog files use | |
| final SaveMetaData metadata = (SaveMetaData) MetaDataFactory.buildMetaData(inputFile); | |
| // Clean the log | |
| final Command cleanedCommand = cleanLog(logCommand); | |
| // Extract metadata - ensure we have a SaveMetaData as that's what .vlog files use | |
| final Object metaDataObject = MetaDataFactory.buildMetaData(inputFile); | |
| if (!(metaDataObject instanceof SaveMetaData)) { | |
| throw new IOException("File '" + inputFile + "' is not a valid VASSAL save/log file."); | |
| } | |
| final SaveMetaData metadata = (SaveMetaData) metaDataObject; | |
| // Clean the log | |
| final Command cleanedCommand = cleanLog(logCommand); |
| <modules> | ||
| <module>vassal-app</module> | ||
| <module>vassal-deprecation</module> | ||
| <module>vassal-doc</module> | ||
| <!-- | ||
| <module>vassal-doc</module> | ||
| --> | ||
| <module>release-prepare</module> |
There was a problem hiding this comment.
This PR is titled/logically focused on log-file cleanup, but it also comments out the vassal-doc Maven module. Removing modules from the reactor affects CI and release builds and seems unrelated to the feature change. Please revert this (or move it to a dedicated build/CI PR with rationale).
| <!-- | ||
| <plugin> | ||
| <artifactId>maven-checkstyle-plugin</artifactId> | ||
| <configuration> | ||
| <configLocation>src/test/resources/checkstyle-checks.xml</configLocation> | ||
| <suppressionsLocation>src/test/resources/checkstyle-suppressions.xml</suppressionsLocation> | ||
| <logViolationsToConsole>true</logViolationsToConsole> | ||
| <!-- consoleOutput=true prints warnings in addition to errors --> | ||
| <consoleOutput>true</consoleOutput> |
There was a problem hiding this comment.
The maven-checkstyle-plugin configuration is now commented out. Disabling static checks in vassal-app changes build quality gates and seems unrelated to adding log cleaning functionality. Please revert this change (or justify it in a separate build-focused PR).
| } | ||
| } | ||
|
|
||
| throw new IOException("Invalid log file format"); |
There was a problem hiding this comment.
The error text "Invalid log file format" is hard-coded and loses context about which file failed. Since this error is surfaced via the UI action, please include the file name/path and use the i18n Resources mechanism (or at least a more specific message) so users can diagnose failures.
| throw new IOException("Invalid log file format"); | |
| throw new IOException("Invalid log file format: " + logFile); |
| WriteErrorDialog.error(ex, outputFile); | ||
| GameModule.getGameModule().warn( | ||
| Resources.getString("BasicLogger.clean_logfile_failed", ex.getMessage())); //$NON-NLS-1$ | ||
| } |
There was a problem hiding this comment.
LogCleaner.cleanLogFile() can currently throw unchecked exceptions (e.g., ClassCastException from metadata casting / NullPointerException from invalid metadata) which won’t be caught here because the handler only catches IOException. To avoid crashing the EDT, either make LogCleaner reliably wrap validation failures in IOException, or broaden this catch to handle RuntimeException and report a user-friendly error.
| } | |
| } | |
| catch (RuntimeException ex) { | |
| WriteErrorDialog.error(ex, outputFile); | |
| GameModule.getGameModule().warn( | |
| Resources.getString("BasicLogger.clean_logfile_failed", ex.getMessage())); //$NON-NLS-1$ | |
| } |
| GameModule.getGameModule().warn( | ||
| Resources.getString("BasicLogger.clean_logfile_failed", ex.getMessage())); //$NON-NLS-1$ |
There was a problem hiding this comment.
The failure warning currently inserts ex.getMessage() into BasicLogger.clean_logfile_failed, but that message can be null and doesn’t identify which file failed. Consider reporting the input/output file path(s) in the warning and logging the exception separately (the detailed error is already shown via WriteErrorDialog).
| GameModule.getGameModule().warn( | |
| Resources.getString("BasicLogger.clean_logfile_failed", ex.getMessage())); //$NON-NLS-1$ | |
| final String fileInfo = inputFile.getPath() + " -> " + outputFile.getPath(); | |
| GameModule.getGameModule().warn( | |
| Resources.getString("BasicLogger.clean_logfile_failed", fileInfo)); //$NON-NLS-1$ |
|
Trying this on my log file did not remove the undo commands. The scan needs to do a recursive descent while keeping track of the levels to match up the undo's with commands. My sample log file is below (vlogs are zips so just change the extension back to vlog). It should load in any module. It's from a scrap module used for test purposes. Ignore the game mismatch and the Bad Data messages. It contains a series of chat messages mixed with some undo's. The chat messages are the easiest way to test undo/redo. Stepping through the log file looks like this: It seems you have pushed some files that are specific to your build environment. In this PR there should only be the java files and VASSAL.properties. I edit the SKIPS line in the Makefile and it builds (release-linux) without changing any other configuration files. Did you forget to push PlayerWindow.java? I didn't see the |
| // Append only the commands we want to keep | ||
| for (int i = 0; i < subCommands.length; i++) { | ||
| if (!toRemove[i]) { | ||
| cleanedRoot.append(subCommands[i]); |
There was a problem hiding this comment.
Use assignment to get rid of the top level NullCommand.
cleanedRoot = cleanedRoot.append(subCommands[i]);
Copilot's solution is far too complicated.
There was a problem hiding this comment.
I wonder whether this PR is desirable from the viewpoint of facilitating cheating in PBEM games, or to put it more kindly, presenting a temptation to those who having a run of bad luck.
I know that this is a controversial topic but it is not the only example of Vassal's design relying on obscurity to keep players on the straight and narrow.
Assuming the legitimate use-case warrants the PR, perhaps some mitigation can be considered. Eg insert a warning into the log when undone actions are removed. Such as a summary at the end of each block of undos.
There was a problem hiding this comment.
Fair PBEM involves considerable faith and trust in opponents' honesty. I had the similar thought about making it easier to redo moves. As it is there are other ways to redo for a better outcome particularily in the PBEM setting.
How often are there legitimate corrections in log files? It seems that this would only be an occasional occurrence. Although in situations where there are multiple overlapping undo/redo steps it can be difficult to follow.
Perhaps a better approach is not writing the output to a file. Instead, filter out the undo commands on playback. Effectively moving the filter from the producer to the consumer. A preference setting could enable this feature. The chat log could output a "skipping undo" message in place of the do/undo block.
No description provided.