Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 21 additions & 25 deletions data/modlist.yaml
Original file line number Diff line number Diff line change
@@ -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"
7 changes: 6 additions & 1 deletion locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<html>Some setup paths were not found in the extracted archive.<br>Correct them below, or dismiss to leave affected mods as empty folders (red X in MO2).</html>",
"ERR_SETUP_PATH_BLANK": "The following paths must not be blank: {0}"
}
104 changes: 98 additions & 6 deletions src/main/java/de/tosox/zonerelay/application/InstallCoordinator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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);

Expand All @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -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<HashMismatch> hashMismatches = new ArrayList<>();
List<SetupFailure> 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"));
Expand All @@ -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");

Expand All @@ -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<HashMismatch> hashMismatches) throws Exception {
List<HashMismatch> hashMismatches, List<SetupFailure> setupFailures) throws Exception {
if (entries == null || entries.isEmpty()) {
return;
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -254,6 +271,81 @@ private void presentHashMismatchSummary(List<HashMismatch> mismatches) {
});
}

private void presentSetupFailureSummary(List<SetupFailure> 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<String, Map<String, JTextField>> 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<String> 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<String, Map<String, String>> corrections = new LinkedHashMap<>();
fieldMap.forEach((modId, pathFields) -> {
Map<String, String> 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");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package de.tosox.zonerelay.domain.model;

import java.util.List;

public record SetupFailure(String modName, String modId, List<String> invalidPaths) {}
Original file line number Diff line number Diff line change
@@ -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<String> missingPaths;

public SetupPathMissingException(List<String> missingPaths) {
super("Setup paths not found: " + missingPaths);
this.missingPaths = Collections.unmodifiableList(missingPaths);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -81,6 +83,20 @@ public void install(ModEntry entry, File archive, ProgressListener progressListe
List<String> setup = mod.getSetup();
int total = setup.size();

List<String> 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, Map<String, String>> 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<String, String> 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());
}
}
}
Loading