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 extends ModEntry> 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 extends ModEntry> 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());
+ }
+ }
+}