From 6a3ea3c1924077639bc5d6760337e909624ec3fc Mon Sep 17 00:00:00 2001 From: Tosox <57193602+Tosox@users.noreply.github.com> Date: Sat, 2 May 2026 14:39:00 +0200 Subject: [PATCH] feat: Collect and recover stale setup paths (#79) Closes #79 --- data/modlist.yaml | 46 ++++---- locales/en-US.json | 7 +- .../application/InstallCoordinator.java | 104 +++++++++++++++++- .../zonerelay/domain/model/SetupFailure.java | 5 + .../model/SetupPathMissingException.java | 16 +++ .../install/ModEntryInstaller.java | 16 +++ .../persistence/ModlistSetupUpdater.java | 65 +++++++++++ 7 files changed, 227 insertions(+), 32 deletions(-) create mode 100644 src/main/java/de/tosox/zonerelay/domain/model/SetupFailure.java create mode 100644 src/main/java/de/tosox/zonerelay/domain/model/SetupPathMissingException.java create mode 100644 src/main/java/de/tosox/zonerelay/infrastructure/persistence/ModlistSetupUpdater.java diff --git a/data/modlist.yaml b/data/modlist.yaml index f768817..3a562c5 100644 --- a/data/modlist.yaml +++ b/data/modlist.yaml @@ -1,29 +1,25 @@ -profileName: VIP +profileName: "VIP" shortcutName: "S.T.A.L.K.E.R. VIP" - mods: - - id: mcm - name: Mod Configuration Menu - url: https://www.moddb.com/mods/stalker-anomaly/addons/anomaly-mod-configuration-menu - setup: [ - "gamedata" - ] - - id: fdda - name: Food, Drug and Drink Animations - url: https://github.com/Grokitach/anomaly_fdda/archive/refs/heads/main.zip - setup: [ - "anomaly_fdda-main/gamedata" - ] + - id: "mcm" + name: "Mod Configuration Menu" + url: "https://www.moddb.com/mods/stalker-anomaly/addons/anomaly-mod-configuration-menu" + setup: + - "gamedata" + - id: "fdda" + name: "Food, Drug and Drink Animations" + url: "https://github.com/Grokitach/anomaly_fdda/archive/refs/heads/main.zip" + setup: + - "anomaly_fdda-main/gamedata" patches: - - id: modded-exes - name: Modded Exes - url: https://github.com/themrdemonized/xray-monolith/releases/download/2024.12.9/STALKER-Anomaly-modded-exes_2024.12.9.zip - setup: [ - "db", - "bin" - ] + - id: "modded-exes" + name: "Modded Exes" + url: "https://github.com/themrdemonized/xray-monolith/releases/download/2024.12.9/STALKER-Anomaly-modded-exes_2024.12.9.zip" + setup: + - "db" + - "bin" separators: - - id: frameworks-separator - name: Frameworks - - id: animations-separator - name: Animations + - id: "frameworks-separator" + name: "Frameworks" + - id: "animations-separator" + name: "Animations" diff --git a/locales/en-US.json b/locales/en-US.json index 60d2185..ebaa157 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -41,5 +41,10 @@ "DLG_HASH_MISMATCH_TITLE": "Hash Mismatch", "DLG_HASH_MISMATCH_UPDATE_TITLE": "Update Hashes", - "DLG_HASH_MISMATCH_UPDATE_MESSAGE": "Would you like to update the mismatched hashes in modlist.yaml with the actual values?" + "DLG_HASH_MISMATCH_UPDATE_MESSAGE": "Would you like to update the mismatched hashes in modlist.yaml with the actual values?", + + "MSG_SETUP_FAILURE_SUMMARY": "\nSetup path failures detected for the following mods:", + "DLG_SETUP_FAILURE_TITLE": "Setup Path Failure", + "DLG_SETUP_FAILURE_DESCRIPTION": "Some setup paths were not found in the extracted archive.
Correct them below, or dismiss to leave affected mods as empty folders (red X in MO2).", + "ERR_SETUP_PATH_BLANK": "The following paths must not be blank: {0}" } diff --git a/src/main/java/de/tosox/zonerelay/application/InstallCoordinator.java b/src/main/java/de/tosox/zonerelay/application/InstallCoordinator.java index 9bbb9f7..ed9205e 100644 --- a/src/main/java/de/tosox/zonerelay/application/InstallCoordinator.java +++ b/src/main/java/de/tosox/zonerelay/application/InstallCoordinator.java @@ -8,10 +8,13 @@ import de.tosox.zonerelay.domain.model.Mod; import de.tosox.zonerelay.domain.model.ModEntry; import de.tosox.zonerelay.domain.model.ModlistConfig; +import de.tosox.zonerelay.domain.model.SetupFailure; +import de.tosox.zonerelay.domain.model.SetupPathMissingException; import de.tosox.zonerelay.domain.port.*; import de.tosox.zonerelay.infrastructure.download.DownloadsManifestStore; import de.tosox.zonerelay.infrastructure.download.ManifestEntry; import de.tosox.zonerelay.infrastructure.persistence.ModlistHashUpdater; +import de.tosox.zonerelay.infrastructure.persistence.ModlistSetupUpdater; import de.tosox.zonerelay.shared.config.AppPaths; import de.tosox.zonerelay.shared.config.ArchiveCleanupStrategy; import de.tosox.zonerelay.shared.config.UserSettings; @@ -22,8 +25,10 @@ import org.apache.commons.io.FileUtils; import javax.swing.*; +import java.awt.*; import java.io.File; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; @@ -45,6 +50,7 @@ public class InstallCoordinator { private final UserSettings userSettings; private final DownloadsManifestStore manifestStore; private final ModlistHashUpdater modlistHashUpdater; + private final ModlistSetupUpdater modlistSetupUpdater; private final AtomicBoolean isInstalling = new AtomicBoolean(false); @@ -61,7 +67,7 @@ public InstallCoordinator(@Named("file") Logger fileLogger, @Named("ui") Logger SplashImageCopier splashImageCopier, ShortcutCreator shortcutCreator, InstallProgressStore progressStore, AppPaths paths, UserSettings userSettings, DownloadsManifestStore manifestStore, - ModlistHashUpdater modlistHashUpdater) { + ModlistHashUpdater modlistHashUpdater, ModlistSetupUpdater modlistSetupUpdater) { this.fileLogger = fileLogger; this.uiLogger = uiLogger; this.localizer = localizer; @@ -75,6 +81,7 @@ public InstallCoordinator(@Named("file") Logger fileLogger, @Named("ui") Logger this.userSettings = userSettings; this.manifestStore = manifestStore; this.modlistHashUpdater = modlistHashUpdater; + this.modlistSetupUpdater = modlistSetupUpdater; } public void startInstallation(ModlistConfig config, boolean fullInstall, String resumeFromId) { @@ -105,13 +112,14 @@ private void runInstallation(ModlistConfig config, boolean fullInstall, String r AtomicInteger completedMods = new AtomicInteger(0); AtomicBoolean resumePointFound = new AtomicBoolean(resumeFromId == null); List hashMismatches = new ArrayList<>(); + List setupFailures = new ArrayList<>(); uiLogger.info("\n================================================================="); uiLogger.info(localizer.translate("MSG_STARTING_INSTALLATION")); uiLogger.info("================================================================="); - installEntries(config.getMods(), fullInstall, totalMods, completedMods, resumeFromId, resumePointFound, hashMismatches); - installEntries(config.getPatches(), fullInstall, totalMods, completedMods, resumeFromId, resumePointFound, hashMismatches); - installEntries(config.getSeparators(), fullInstall, totalMods, completedMods, resumeFromId, resumePointFound, hashMismatches); + installEntries(config.getMods(), fullInstall, totalMods, completedMods, resumeFromId, resumePointFound, hashMismatches, setupFailures); + installEntries(config.getPatches(), fullInstall, totalMods, completedMods, resumeFromId, resumePointFound, hashMismatches, setupFailures); + installEntries(config.getSeparators(), fullInstall, totalMods, completedMods, resumeFromId, resumePointFound, hashMismatches, setupFailures); uiLogger.info("\n================================================================="); uiLogger.info(localizer.translate("MSG_INSTALLATION_MO2_SETUP")); @@ -129,6 +137,10 @@ private void runInstallation(ModlistConfig config, boolean fullInstall, String r presentHashMismatchSummary(hashMismatches); } + if (!setupFailures.isEmpty()) { + presentSetupFailureSummary(setupFailures); + } + uiLogger.info(localizer.translate("MSG_COMPLETE_INSTALLATION")); fileLogger.info("Installation completed successfully"); @@ -138,7 +150,7 @@ private void runInstallation(ModlistConfig config, boolean fullInstall, String r private void installEntries(List entries, boolean fullInstall, int totalMods, AtomicInteger completedMods, String resumeFromId, AtomicBoolean resumePointFound, - List hashMismatches) throws Exception { + List hashMismatches, List setupFailures) throws Exception { if (entries == null || entries.isEmpty()) { return; } @@ -186,7 +198,12 @@ private void installEntries(List entries, boolean fullInstal } } - findInstaller(entry).install(entry, archive, currentProgressListener); + try { + findInstaller(entry).install(entry, archive, currentProgressListener); + } catch (SetupPathMissingException e) { + setupFailures.add(new SetupFailure(mod.getName(), mod.getId(), e.getMissingPaths())); + fileLogger.warn("Setup path(s) missing for %s: %s", mod.getName(), e.getMissingPaths()); + } manifestStore.recordInstall(mod.getId(), result.computedHash()); applyArchiveCleanup(result, previousEntry); @@ -254,6 +271,81 @@ private void presentHashMismatchSummary(List mismatches) { }); } + private void presentSetupFailureSummary(List failures) { + StringBuilder sb = new StringBuilder(localizer.translate("MSG_SETUP_FAILURE_SUMMARY")); + for (SetupFailure f : failures) { + sb.append("\n ").append(f.modName()).append(":"); + for (String path : f.invalidPaths()) { + sb.append("\n ").append(path); + } + } + String logMessage = sb.toString(); + uiLogger.warn(logMessage); + fileLogger.warn(logMessage); + + SwingUtilities.invokeLater(() -> { + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel description = new JLabel(localizer.translate("DLG_SETUP_FAILURE_DESCRIPTION")); + description.setAlignmentX(Component.LEFT_ALIGNMENT); + panel.add(description); + + Map> fieldMap = new LinkedHashMap<>(); + for (SetupFailure f : failures) { + panel.add(Box.createVerticalStrut(10)); + + JLabel modHeader = new JLabel(f.modName()); + modHeader.setFont(modHeader.getFont().deriveFont(Font.BOLD)); + modHeader.setAlignmentX(Component.LEFT_ALIGNMENT); + panel.add(modHeader); + + JPanel pathsPanel = new JPanel(new GridLayout(0, 2, 4, 2)); + pathsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + fieldMap.put(f.modId(), new LinkedHashMap<>()); + for (String path : f.invalidPaths()) { + pathsPanel.add(new JLabel(path + ":")); + JTextField field = new JTextField(path, 40); + fieldMap.get(f.modId()).put(path, field); + pathsPanel.add(field); + } + panel.add(pathsPanel); + } + + while (true) { + int choice = JOptionPane.showConfirmDialog(null, new JScrollPane(panel), + localizer.translate("DLG_SETUP_FAILURE_TITLE"), + JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE); + if (choice != JOptionPane.OK_OPTION) { + break; + } + + List blankFields = fieldMap.values().stream() + .flatMap(m -> m.entrySet().stream()) + .filter(e -> e.getValue().getText().trim().isEmpty()) + .map(Map.Entry::getKey) + .toList(); + + if (!blankFields.isEmpty()) { + JOptionPane.showMessageDialog(null, + localizer.translate("ERR_SETUP_PATH_BLANK", String.join(", ", blankFields)), + localizer.translate("DLG_SETUP_FAILURE_TITLE"), JOptionPane.ERROR_MESSAGE); + continue; + } + + Map> corrections = new LinkedHashMap<>(); + fieldMap.forEach((modId, pathFields) -> { + Map modCorrections = new LinkedHashMap<>(); + pathFields.forEach((oldPath, field) -> modCorrections.put(oldPath, field.getText().trim())); + corrections.put(modId, modCorrections); + }); + new Thread(() -> modlistSetupUpdater.updateSetup(paths.getModlistYaml(), corrections), + "modlist-setup-update").start(); + break; + } + }); + } + private void setupMo2Environment(ModlistConfig config) { uiLogger.info(localizer.translate("MSG_CREATE_CUSTOM_PROFILE")); fileLogger.info("Setting up MO2 profile"); diff --git a/src/main/java/de/tosox/zonerelay/domain/model/SetupFailure.java b/src/main/java/de/tosox/zonerelay/domain/model/SetupFailure.java new file mode 100644 index 0000000..31e3537 --- /dev/null +++ b/src/main/java/de/tosox/zonerelay/domain/model/SetupFailure.java @@ -0,0 +1,5 @@ +package de.tosox.zonerelay.domain.model; + +import java.util.List; + +public record SetupFailure(String modName, String modId, List invalidPaths) {} diff --git a/src/main/java/de/tosox/zonerelay/domain/model/SetupPathMissingException.java b/src/main/java/de/tosox/zonerelay/domain/model/SetupPathMissingException.java new file mode 100644 index 0000000..f46296d --- /dev/null +++ b/src/main/java/de/tosox/zonerelay/domain/model/SetupPathMissingException.java @@ -0,0 +1,16 @@ +package de.tosox.zonerelay.domain.model; + +import lombok.Getter; + +import java.util.Collections; +import java.util.List; + +@Getter +public class SetupPathMissingException extends Exception { + private final List missingPaths; + + public SetupPathMissingException(List missingPaths) { + super("Setup paths not found: " + missingPaths); + this.missingPaths = Collections.unmodifiableList(missingPaths); + } +} diff --git a/src/main/java/de/tosox/zonerelay/infrastructure/install/ModEntryInstaller.java b/src/main/java/de/tosox/zonerelay/infrastructure/install/ModEntryInstaller.java index 9486ec1..e006e79 100644 --- a/src/main/java/de/tosox/zonerelay/infrastructure/install/ModEntryInstaller.java +++ b/src/main/java/de/tosox/zonerelay/infrastructure/install/ModEntryInstaller.java @@ -6,6 +6,7 @@ import de.tosox.zonerelay.domain.model.EntryType; import de.tosox.zonerelay.domain.model.Mod; import de.tosox.zonerelay.domain.model.ModEntry; +import de.tosox.zonerelay.domain.model.SetupPathMissingException; import de.tosox.zonerelay.domain.port.ArchiveExtractor; import de.tosox.zonerelay.domain.port.MetaIniWriter; import de.tosox.zonerelay.domain.port.ModInstaller; @@ -20,6 +21,7 @@ import java.io.File; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; @Singleton @@ -81,6 +83,20 @@ public void install(ModEntry entry, File archive, ProgressListener progressListe List setup = mod.getSetup(); int total = setup.size(); + List missingPaths = new ArrayList<>(); + for (String instruction : setup) { + SetupMapping mapping = resolveMapping(instruction, tempDir, targetDir); + if (!Files.exists(mapping.source())) { + missingPaths.add(instruction); + } + } + if (!missingPaths.isEmpty()) { + if (mod.getType() == EntryType.MOD) { + Files.createDirectories(targetDir); + } + throw new SetupPathMissingException(missingPaths); + } + for (int i = 0; i < total; i++) { String instruction = setup.get(i); SetupMapping mapping = resolveMapping(instruction, tempDir, targetDir); diff --git a/src/main/java/de/tosox/zonerelay/infrastructure/persistence/ModlistSetupUpdater.java b/src/main/java/de/tosox/zonerelay/infrastructure/persistence/ModlistSetupUpdater.java new file mode 100644 index 0000000..7aef700 --- /dev/null +++ b/src/main/java/de/tosox/zonerelay/infrastructure/persistence/ModlistSetupUpdater.java @@ -0,0 +1,65 @@ +package de.tosox.zonerelay.infrastructure.persistence; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import de.tosox.zonerelay.shared.logging.Logger; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +@Singleton +public class ModlistSetupUpdater { + private static final ObjectMapper MAPPER = new ObjectMapper( + YAMLFactory.builder() + .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER) + .build()); + + private final Logger logger; + + @Inject + public ModlistSetupUpdater(@Named("file") Logger logger) { + this.logger = logger; + } + + public void updateSetup(Path modlistPath, Map> idToCorrections) { + try { + JsonNode root = MAPPER.readTree(modlistPath.toFile()); + for (String listKey : List.of("mods", "patches")) { + JsonNode list = root.get(listKey); + if (list == null) { + continue; + } + for (JsonNode node : list) { + String id = node.path("id").asText(null); + if (id == null || !idToCorrections.containsKey(id)) { + continue; + } + Map corrections = idToCorrections.get(id); + ArrayNode setupNode = (ArrayNode) node.get("setup"); + if (setupNode == null) { + continue; + } + ArrayNode updated = MAPPER.createArrayNode(); + for (JsonNode entry : setupNode) { + String path = entry.asText(); + updated.add(corrections.getOrDefault(path, path)); + } + ((ObjectNode) node).set("setup", updated); + } + } + MAPPER.writerWithDefaultPrettyPrinter().writeValue(modlistPath.toFile(), root); + logger.info("Updated setup paths for %d mod(s) in modlist.yaml", idToCorrections.size()); + } catch (IOException e) { + logger.error("Failed to update modlist.yaml setup paths: %s", e.getMessage()); + } + } +}