From 92d094e64cc515fdc8bdbedca284f4d01fc2eaf8 Mon Sep 17 00:00:00 2001 From: JustABiologist Date: Tue, 12 May 2026 14:47:43 +0200 Subject: [PATCH 1/4] Add DayZ workshop mod updates --- .../client/configs/UpdaterConfig.java | 4 + .../tasks/updater/mods/DayZWorkshopMod.java | 89 +++++++++++++++++++ .../tasks/updater/mods/TaskModsUpdater.java | 63 ++++++++++++- .../autoplug/client/utils/SteamCMD.java | 61 ++++++++++++- .../updater/mods/DayZWorkshopModTest.java | 68 ++++++++++++++ .../autoplug/client/utils/SteamCMDTest.java | 11 ++- 6 files changed, 291 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/osiris/autoplug/client/tasks/updater/mods/DayZWorkshopMod.java create mode 100644 src/test/java/com/osiris/autoplug/client/tasks/updater/mods/DayZWorkshopModTest.java diff --git a/src/main/java/com/osiris/autoplug/client/configs/UpdaterConfig.java b/src/main/java/com/osiris/autoplug/client/configs/UpdaterConfig.java index 45437a41..7729e889 100644 --- a/src/main/java/com/osiris/autoplug/client/configs/UpdaterConfig.java +++ b/src/main/java/com/osiris/autoplug/client/configs/UpdaterConfig.java @@ -64,6 +64,7 @@ public class UpdaterConfig extends MyYaml { public YamlSection mods_updater_version; public YamlSection mods_updater_async; public YamlSection mods_update_check_name_for_mod_loader; + public YamlSection mods_updater_dayz_workshop_app_id; public UpdaterConfig() throws IOException, DuplicateKeyException, YamlReaderException, IllegalListException, NotLoadedException, IllegalKeyException, YamlWriterException { @@ -230,6 +231,9 @@ public UpdaterConfig() throws IOException, DuplicateKeyException, YamlReaderExce mods_update_check_name_for_mod_loader = put(name, "mods-updater", "check-name-for-mod-loader").setDefValues("false").setComments( "Only relevant for determining if a curseforge mod release is forge or fabric.", "If enabled additionally checks the mod name to see if it contains fabric or forge."); + mods_updater_dayz_workshop_app_id = put(name, "mods-updater", "dayz-workshop-app-id").setDefValues("221100").setComments( + "Steam workshop app-id used for DayZ mods.", + "When the mods path contains DayZ mod folders with meta.cpp files, AutoPlug reads their publishedid and updates them through SteamCMD."); save(); unlockFile(); diff --git a/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/DayZWorkshopMod.java b/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/DayZWorkshopMod.java new file mode 100644 index 00000000..9523742e --- /dev/null +++ b/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/DayZWorkshopMod.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2024 Osiris-Team. + * All rights reserved. + * + * This software is copyrighted work, licensed under the terms + * of the MIT-License. Consult the "LICENSE" file for details. + */ + +package com.osiris.autoplug.client.tasks.updater.mods; + +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +public class DayZWorkshopMod { + private final File directory; + private final String name; + private final String publishedId; + + public DayZWorkshopMod(File directory, String name, String publishedId) { + this.directory = directory; + this.name = name; + this.publishedId = publishedId; + } + + public File getDirectory() { + return directory; + } + + public String getName() { + return name; + } + + public String getPublishedId() { + return publishedId; + } + + @NotNull + public static List findIn(File dir) throws IOException { + if (!dir.exists()) throw new FileNotFoundException("Directory does not exist: " + dir); + List mods = new ArrayList<>(); + File[] files = dir.listFiles(); + if (files == null) return mods; + Arrays.sort(files, Comparator.comparing(File::getName)); + for (File file : files) { + if (!file.isDirectory()) continue; + File metaFile = new File(file, "meta.cpp"); + if (metaFile.exists()) + mods.add(readFromMeta(file, metaFile)); + } + return mods; + } + + static DayZWorkshopMod readFromMeta(File modDir, File metaFile) throws IOException { + String name = modDir.getName(); + String publishedId = null; + for (String line : Files.readAllLines(metaFile.toPath(), StandardCharsets.UTF_8)) { + String trimmedLine = line.trim(); + if (trimmedLine.isEmpty() || trimmedLine.startsWith("//")) continue; + + int equalsIndex = trimmedLine.indexOf('='); + if (equalsIndex < 0) continue; + + String key = trimmedLine.substring(0, equalsIndex).trim(); + String value = trimmedLine.substring(equalsIndex + 1).trim(); + int semicolonIndex = value.indexOf(';'); + if (semicolonIndex < 0) continue; + value = value.substring(0, semicolonIndex).trim(); + if (value.startsWith("\"") && value.endsWith("\"") && value.length() >= 2) + value = value.substring(1, value.length() - 1); + + if (key.equals("name")) name = value; + if (key.equals("publishedid")) publishedId = value; + } + + if (publishedId == null || !publishedId.matches("\\d+")) + throw new IOException("Failed to read publishedid from " + metaFile); + + return new DayZWorkshopMod(modDir, name, publishedId); + } +} diff --git a/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/TaskModsUpdater.java b/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/TaskModsUpdater.java index 6d0b078c..143b671a 100644 --- a/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/TaskModsUpdater.java +++ b/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/TaskModsUpdater.java @@ -15,6 +15,7 @@ import com.osiris.autoplug.client.tasks.updater.plugins.ResourceFinder; import com.osiris.autoplug.client.tasks.updater.search.SearchResult; import com.osiris.autoplug.client.utils.GD; +import com.osiris.autoplug.client.utils.SteamCMD; import com.osiris.autoplug.client.utils.UtilsFile; import com.osiris.autoplug.client.utils.UtilsMinecraft; import com.osiris.betterthread.BThread; @@ -22,6 +23,8 @@ import com.osiris.betterthread.BWarning; import com.osiris.dyml.YamlSection; import com.osiris.dyml.exceptions.DuplicateKeyException; +import com.osiris.jlib.logger.AL; +import org.apache.commons.io.FileUtils; import org.jetbrains.annotations.NotNull; import java.io.DataInputStream; @@ -81,7 +84,14 @@ public void runAtStart() throws Exception { setStatus("Fetching latest mod data..."); userProfile = updaterConfig.mods_updater_profile.asString(); - this.allMods.addAll(new UtilsMinecraft().getMods(FileManager.convertRelativeToAbsolutePath(updaterConfig.mods_updater_path.asString()))); + File modsDir = FileManager.convertRelativeToAbsolutePath(updaterConfig.mods_updater_path.asString()); + List dayZWorkshopMods = DayZWorkshopMod.findIn(modsDir); + if (!dayZWorkshopMods.isEmpty()) { + doDayZWorkshopUpdateLogic(dayZWorkshopMods); + return; + } + + this.allMods.addAll(new UtilsMinecraft().getMods(modsDir)); for (MinecraftMod installedMod : allMods) { @@ -337,6 +347,57 @@ else if (code == SearchResult.Type.RESOURCE_NOT_FOUND) } + private void doDayZWorkshopUpdateLogic(@NotNull List dayZWorkshopMods) throws Exception { + setMax(dayZWorkshopMods.size()); + String configuredWorkshopAppId = updaterConfig.mods_updater_dayz_workshop_app_id.asString(); + String workshopAppId = configuredWorkshopAppId == null || configuredWorkshopAppId.isEmpty() ? "221100" : configuredWorkshopAppId; + + if (userProfile.equals(notifyProfile)) { + for (DayZWorkshopMod mod : dayZWorkshopMods) + addInfo("NOTIFY: DayZ mod '" + mod.getName() + "' can be updated with Steam Workshop item " + mod.getPublishedId() + "."); + finish("Found " + dayZWorkshopMods.size() + " DayZ workshop mods."); + return; + } + + SteamCMD steamCMD = new SteamCMD(); + int successfulUpdates = 0; + int checkedMods = 0; + for (DayZWorkshopMod mod : dayZWorkshopMods) { + checkedMods++; + setStatus("Updating DayZ mod '" + mod.getName() + "' (" + checkedMods + "/" + dayZWorkshopMods.size() + ")..."); + boolean isSuccess = steamCMD.installOrUpdateWorkshopItem(workshopAppId, mod.getPublishedId(), line -> { + AL.debug(this.getClass(), "SteamCMD-Out: " + line); + setStatus(line); + }, errLine -> { + AL.debug(this.getClass(), "SteamCMD-Err-Out: " + errLine); + setStatus(errLine); + addWarning(errLine); + }); + if (!isSuccess) { + addWarning("Failed to update DayZ mod '" + mod.getName() + "' via SteamCMD."); + continue; + } + + File downloadedDir = steamCMD.getWorkshopItemDir(workshopAppId, mod.getPublishedId()); + if (userProfile.equals(manualProfile)) { + addInfo("MANUAL: Downloaded DayZ mod '" + mod.getName() + "' to " + downloadedDir.getAbsolutePath() + "."); + } else { + setStatus("Copying DayZ mod '" + mod.getName() + "' into " + mod.getDirectory().getAbsolutePath() + "..."); + FileUtils.copyDirectory(downloadedDir, mod.getDirectory()); + addInfo("Updated DayZ mod '" + mod.getName() + "' from Steam Workshop item " + mod.getPublishedId() + "."); + } + successfulUpdates++; + } + + if (successfulUpdates == dayZWorkshopMods.size()) { + setSuccess(true); + finish("Updated " + successfulUpdates + " DayZ workshop mods."); + } else { + setSuccess(false); + finish("Updated " + successfulUpdates + "/" + dayZWorkshopMods.size() + " DayZ workshop mods."); + } + } + private void doDownloadLogic(@NotNull MinecraftMod mod, SearchResult result) { SearchResult.Type code = result.type; String type = result.getDownloadType(); // The file type to download (Note: When 'external' is returned nothing will be downloaded. Working on a fix for this!) diff --git a/src/main/java/com/osiris/autoplug/client/utils/SteamCMD.java b/src/main/java/com/osiris/autoplug/client/utils/SteamCMD.java index 9c971e52..6b0f8dfa 100644 --- a/src/main/java/com/osiris/autoplug/client/utils/SteamCMD.java +++ b/src/main/java/com/osiris/autoplug/client/utils/SteamCMD.java @@ -35,6 +35,7 @@ @SuppressWarnings({"WeakerAccess", "unused"}) public class SteamCMD { + private static final String STEAMCMD_WORKSHOP_COMMAND = "+login {LOGIN} +workshop_download_item {WORKSHOP_APP} {WORKSHOP_ITEM} validate +quit"; private final String steamcmdArchive = "steamcmd" + (isWindows ? ".zip" : isMac ? "_osx.tar.gz" : "_linux.tar.gz"); private final String steamcmdExtension = isWindows ? ".exe" : ".sh"; private final String steamcmdExecutable = "steamcmd" + steamcmdExtension; @@ -115,8 +116,7 @@ public boolean installOrUpdateServer(String appId, Consumer onLog, Consu AL.debug(this.getClass(), "Installing app " + appId + "..."); onLog.accept("Installing app " + appId + "..."); - String login = new UpdaterConfig().server_steamcmd_login.asString(); - if (login == null || login.isEmpty()) login = "anonymous"; + String login = getLogin(); File gameInstallDir = new File(dirSteamServersDownloads + "/" + appId); gameInstallDir.mkdirs(); @@ -159,10 +159,65 @@ public boolean installOrUpdateServer(String appId, Consumer onLog, Consu } } + public boolean installOrUpdateWorkshopItem(String workshopAppId, String workshopItemId, Consumer onLog, Consumer onLogErr) { + try { + if (!installIfNeeded()) return false; + AL.debug(this.getClass(), "Installing workshop item " + workshopItemId + " for app " + workshopAppId + "..."); + onLog.accept("Installing workshop item " + workshopItemId + "..."); + + String command = buildWorkshopItemCommand(getLogin(), workshopAppId, workshopItemId); + List logLines = new ArrayList<>(); + List logErrLines = new ArrayList<>(); + AtomicBoolean isFinished = new AtomicBoolean(false); + AtomicBoolean isSuccess = new AtomicBoolean(true); + AsyncTerminal terminal = new AsyncTerminal(destDir, line -> { + onLog.accept(line); + logLines.add(line); + String lowerLine = line.toLowerCase(); + if (lowerLine.startsWith("success.") && lowerLine.contains("item " + workshopItemId.toLowerCase())) + isFinished.set(true); + if (lowerLine.startsWith("error!")) { + isSuccess.set(false); + isFinished.set(true); + } + }, line -> { + onLogErr.accept(line); + logErrLines.add(line); + }, destExe.getAbsolutePath() + " " + command); + + Thread thread = new Thread(() -> terminal.process.destroy()); + Runtime.getRuntime().addShutdownHook(thread); + while (!isFinished.get() && terminal.process.isAlive()) Thread.sleep(100); + if (terminal.process.isAlive()) terminal.process.destroy(); + Runtime.getRuntime().removeShutdownHook(thread); + return isSuccess.get() && getWorkshopItemDir(workshopAppId, workshopItemId).exists(); + } catch (Exception e) { + AL.warn(e); + return false; + } + } + + public File getWorkshopItemDir(String workshopAppId, String workshopItemId) { + return new File(destDir + "/steamapps/workshop/content/" + workshopAppId + "/" + workshopItemId); + } + + static String buildWorkshopItemCommand(String login, String workshopAppId, String workshopItemId) { + return STEAMCMD_WORKSHOP_COMMAND + .replace("{LOGIN}", login) + .replace("{WORKSHOP_APP}", workshopAppId) + .replace("{WORKSHOP_ITEM}", workshopItemId); + } + + private String getLogin() throws NotLoadedException, YamlReaderException, YamlWriterException, IOException, IllegalKeyException, DuplicateKeyException, IllegalListException { + String login = new UpdaterConfig().server_steamcmd_login.asString(); + if (login == null || login.isEmpty()) login = "anonymous"; + return login; + } + public String getResolutionForError(String error) { for (Map.Entry entry : errorResolutions.entrySet()) if (error.contains(entry.getKey())) return entry.getValue(); return "Unknown. :("; } -} \ No newline at end of file +} diff --git a/src/test/java/com/osiris/autoplug/client/tasks/updater/mods/DayZWorkshopModTest.java b/src/test/java/com/osiris/autoplug/client/tasks/updater/mods/DayZWorkshopModTest.java new file mode 100644 index 00000000..75a74627 --- /dev/null +++ b/src/test/java/com/osiris/autoplug/client/tasks/updater/mods/DayZWorkshopModTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024 Osiris-Team. + * All rights reserved. + * + * This software is copyrighted work, licensed under the terms + * of the MIT-License. Consult the "LICENSE" file for details. + */ + +package com.osiris.autoplug.client.tasks.updater.mods; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class DayZWorkshopModTest { + + @TempDir + Path tempDir; + + @Test + void readsPublishedIdAndNameFromMetaCpp() throws Exception { + File modDir = tempDir.resolve("@CF").toFile(); + modDir.mkdirs(); + File metaFile = writeMeta(modDir, + "protocol = 1;", + "publishedid = 1559212036;", + "name = \"CF\";", + "timestamp = 5249804932187309401;"); + + DayZWorkshopMod mod = DayZWorkshopMod.readFromMeta(modDir, metaFile); + + assertEquals(modDir, mod.getDirectory()); + assertEquals("CF", mod.getName()); + assertEquals("1559212036", mod.getPublishedId()); + } + + @Test + void findsModsWithMetaCppInDirectSubdirectories() throws Exception { + File firstModDir = tempDir.resolve("@CF").toFile(); + File secondModDir = tempDir.resolve("@CommunityOnlineTools").toFile(); + File ignoredDir = tempDir.resolve("keys").toFile(); + firstModDir.mkdirs(); + secondModDir.mkdirs(); + ignoredDir.mkdirs(); + writeMeta(firstModDir, "publishedid = 1559212036;", "name = \"CF\";"); + writeMeta(secondModDir, "publishedid = 1564026768;", "name = \"Community-Online-Tools\";"); + + List mods = DayZWorkshopMod.findIn(tempDir.toFile()); + + assertEquals(2, mods.size()); + assertEquals("1559212036", mods.get(0).getPublishedId()); + assertEquals("1564026768", mods.get(1).getPublishedId()); + } + + private File writeMeta(File modDir, String... lines) throws Exception { + File metaFile = new File(modDir, "meta.cpp"); + Files.write(metaFile.toPath(), Arrays.asList(lines), StandardCharsets.UTF_8); + return metaFile; + } +} diff --git a/src/test/java/com/osiris/autoplug/client/utils/SteamCMDTest.java b/src/test/java/com/osiris/autoplug/client/utils/SteamCMDTest.java index 7d39cf1f..95c1ca44 100644 --- a/src/test/java/com/osiris/autoplug/client/utils/SteamCMDTest.java +++ b/src/test/java/com/osiris/autoplug/client/utils/SteamCMDTest.java @@ -13,11 +13,20 @@ import java.io.IOException; +import static org.junit.jupiter.api.Assertions.assertEquals; + class SteamCMDTest { + @Test + void buildsWorkshopItemCommand() { + String command = SteamCMD.buildWorkshopItemCommand("anonymous", "221100", "1559212036"); + + assertEquals("+login anonymous +workshop_download_item 221100 1559212036 validate +quit", command); + } + @Test void installSteamcmd() throws IOException { UtilsTest.init(); new SteamCMD().installIfNeeded(); } -} \ No newline at end of file +} From b6604e275ae60b8abe508fd7c970576a27158546 Mon Sep 17 00:00:00 2001 From: JustABiologist Date: Thu, 14 May 2026 23:14:56 +0200 Subject: [PATCH 2/4] Generalize Steam Workshop mod updater --- .../client/configs/UpdaterConfig.java | 7 ++- ...WorkshopMod.java => SteamWorkshopMod.java} | 12 +++--- .../tasks/updater/mods/TaskModsUpdater.java | 43 +++++++++++-------- ...ModTest.java => SteamWorkshopModTest.java} | 6 +-- 4 files changed, 36 insertions(+), 32 deletions(-) rename src/main/java/com/osiris/autoplug/client/tasks/updater/mods/{DayZWorkshopMod.java => SteamWorkshopMod.java} (86%) rename src/test/java/com/osiris/autoplug/client/tasks/updater/mods/{DayZWorkshopModTest.java => SteamWorkshopModTest.java} (91%) diff --git a/src/main/java/com/osiris/autoplug/client/configs/UpdaterConfig.java b/src/main/java/com/osiris/autoplug/client/configs/UpdaterConfig.java index 7729e889..6c815a82 100644 --- a/src/main/java/com/osiris/autoplug/client/configs/UpdaterConfig.java +++ b/src/main/java/com/osiris/autoplug/client/configs/UpdaterConfig.java @@ -64,7 +64,6 @@ public class UpdaterConfig extends MyYaml { public YamlSection mods_updater_version; public YamlSection mods_updater_async; public YamlSection mods_update_check_name_for_mod_loader; - public YamlSection mods_updater_dayz_workshop_app_id; public UpdaterConfig() throws IOException, DuplicateKeyException, YamlReaderException, IllegalListException, NotLoadedException, IllegalKeyException, YamlWriterException { @@ -231,9 +230,9 @@ public UpdaterConfig() throws IOException, DuplicateKeyException, YamlReaderExce mods_update_check_name_for_mod_loader = put(name, "mods-updater", "check-name-for-mod-loader").setDefValues("false").setComments( "Only relevant for determining if a curseforge mod release is forge or fabric.", "If enabled additionally checks the mod name to see if it contains fabric or forge."); - mods_updater_dayz_workshop_app_id = put(name, "mods-updater", "dayz-workshop-app-id").setDefValues("221100").setComments( - "Steam workshop app-id used for DayZ mods.", - "When the mods path contains DayZ mod folders with meta.cpp files, AutoPlug reads their publishedid and updates them through SteamCMD."); + mods_updater_path.setComments( + "Path to your mods folder.", + "Steam Workshop mods with supported metadata can be updated through SteamCMD when server-updater.software is set to a numeric Steam app-id."); save(); unlockFile(); diff --git a/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/DayZWorkshopMod.java b/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/SteamWorkshopMod.java similarity index 86% rename from src/main/java/com/osiris/autoplug/client/tasks/updater/mods/DayZWorkshopMod.java rename to src/main/java/com/osiris/autoplug/client/tasks/updater/mods/SteamWorkshopMod.java index 9523742e..559ab459 100644 --- a/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/DayZWorkshopMod.java +++ b/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/SteamWorkshopMod.java @@ -20,12 +20,12 @@ import java.util.Comparator; import java.util.List; -public class DayZWorkshopMod { +public class SteamWorkshopMod { private final File directory; private final String name; private final String publishedId; - public DayZWorkshopMod(File directory, String name, String publishedId) { + public SteamWorkshopMod(File directory, String name, String publishedId) { this.directory = directory; this.name = name; this.publishedId = publishedId; @@ -44,9 +44,9 @@ public String getPublishedId() { } @NotNull - public static List findIn(File dir) throws IOException { + public static List findIn(File dir) throws IOException { if (!dir.exists()) throw new FileNotFoundException("Directory does not exist: " + dir); - List mods = new ArrayList<>(); + List mods = new ArrayList<>(); File[] files = dir.listFiles(); if (files == null) return mods; Arrays.sort(files, Comparator.comparing(File::getName)); @@ -59,7 +59,7 @@ public static List findIn(File dir) throws IOException { return mods; } - static DayZWorkshopMod readFromMeta(File modDir, File metaFile) throws IOException { + static SteamWorkshopMod readFromMeta(File modDir, File metaFile) throws IOException { String name = modDir.getName(); String publishedId = null; for (String line : Files.readAllLines(metaFile.toPath(), StandardCharsets.UTF_8)) { @@ -84,6 +84,6 @@ static DayZWorkshopMod readFromMeta(File modDir, File metaFile) throws IOExcepti if (publishedId == null || !publishedId.matches("\\d+")) throw new IOException("Failed to read publishedid from " + metaFile); - return new DayZWorkshopMod(modDir, name, publishedId); + return new SteamWorkshopMod(modDir, name, publishedId); } } diff --git a/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/TaskModsUpdater.java b/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/TaskModsUpdater.java index 143b671a..01a97312 100644 --- a/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/TaskModsUpdater.java +++ b/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/TaskModsUpdater.java @@ -85,9 +85,9 @@ public void runAtStart() throws Exception { userProfile = updaterConfig.mods_updater_profile.asString(); File modsDir = FileManager.convertRelativeToAbsolutePath(updaterConfig.mods_updater_path.asString()); - List dayZWorkshopMods = DayZWorkshopMod.findIn(modsDir); - if (!dayZWorkshopMods.isEmpty()) { - doDayZWorkshopUpdateLogic(dayZWorkshopMods); + List steamWorkshopMods = SteamWorkshopMod.findIn(modsDir); + if (!steamWorkshopMods.isEmpty()) { + doSteamWorkshopUpdateLogic(steamWorkshopMods); return; } @@ -347,24 +347,29 @@ else if (code == SearchResult.Type.RESOURCE_NOT_FOUND) } - private void doDayZWorkshopUpdateLogic(@NotNull List dayZWorkshopMods) throws Exception { - setMax(dayZWorkshopMods.size()); - String configuredWorkshopAppId = updaterConfig.mods_updater_dayz_workshop_app_id.asString(); - String workshopAppId = configuredWorkshopAppId == null || configuredWorkshopAppId.isEmpty() ? "221100" : configuredWorkshopAppId; + private void doSteamWorkshopUpdateLogic(@NotNull List steamWorkshopMods) throws Exception { + setMax(steamWorkshopMods.size()); + String workshopAppId = updaterConfig.server_software.asString(); + if (workshopAppId == null || !workshopAppId.matches("\\d+")) { + setSuccess(false); + addWarning("Steam Workshop mods were found, but server-updater.software is not a numeric Steam app-id."); + finish("Found " + steamWorkshopMods.size() + " Steam Workshop mods, but no Steam app-id is configured."); + return; + } if (userProfile.equals(notifyProfile)) { - for (DayZWorkshopMod mod : dayZWorkshopMods) - addInfo("NOTIFY: DayZ mod '" + mod.getName() + "' can be updated with Steam Workshop item " + mod.getPublishedId() + "."); - finish("Found " + dayZWorkshopMods.size() + " DayZ workshop mods."); + for (SteamWorkshopMod mod : steamWorkshopMods) + addInfo("NOTIFY: Steam Workshop mod '" + mod.getName() + "' can be updated with Workshop item " + mod.getPublishedId() + " for app " + workshopAppId + "."); + finish("Found " + steamWorkshopMods.size() + " Steam Workshop mods."); return; } SteamCMD steamCMD = new SteamCMD(); int successfulUpdates = 0; int checkedMods = 0; - for (DayZWorkshopMod mod : dayZWorkshopMods) { + for (SteamWorkshopMod mod : steamWorkshopMods) { checkedMods++; - setStatus("Updating DayZ mod '" + mod.getName() + "' (" + checkedMods + "/" + dayZWorkshopMods.size() + ")..."); + setStatus("Updating Steam Workshop mod '" + mod.getName() + "' (" + checkedMods + "/" + steamWorkshopMods.size() + ")..."); boolean isSuccess = steamCMD.installOrUpdateWorkshopItem(workshopAppId, mod.getPublishedId(), line -> { AL.debug(this.getClass(), "SteamCMD-Out: " + line); setStatus(line); @@ -374,27 +379,27 @@ private void doDayZWorkshopUpdateLogic(@NotNull List dayZWorksh addWarning(errLine); }); if (!isSuccess) { - addWarning("Failed to update DayZ mod '" + mod.getName() + "' via SteamCMD."); + addWarning("Failed to update Steam Workshop mod '" + mod.getName() + "' via SteamCMD."); continue; } File downloadedDir = steamCMD.getWorkshopItemDir(workshopAppId, mod.getPublishedId()); if (userProfile.equals(manualProfile)) { - addInfo("MANUAL: Downloaded DayZ mod '" + mod.getName() + "' to " + downloadedDir.getAbsolutePath() + "."); + addInfo("MANUAL: Downloaded Steam Workshop mod '" + mod.getName() + "' to " + downloadedDir.getAbsolutePath() + "."); } else { - setStatus("Copying DayZ mod '" + mod.getName() + "' into " + mod.getDirectory().getAbsolutePath() + "..."); + setStatus("Copying Steam Workshop mod '" + mod.getName() + "' into " + mod.getDirectory().getAbsolutePath() + "..."); FileUtils.copyDirectory(downloadedDir, mod.getDirectory()); - addInfo("Updated DayZ mod '" + mod.getName() + "' from Steam Workshop item " + mod.getPublishedId() + "."); + addInfo("Updated Steam Workshop mod '" + mod.getName() + "' from Workshop item " + mod.getPublishedId() + "."); } successfulUpdates++; } - if (successfulUpdates == dayZWorkshopMods.size()) { + if (successfulUpdates == steamWorkshopMods.size()) { setSuccess(true); - finish("Updated " + successfulUpdates + " DayZ workshop mods."); + finish("Updated " + successfulUpdates + " Steam Workshop mods."); } else { setSuccess(false); - finish("Updated " + successfulUpdates + "/" + dayZWorkshopMods.size() + " DayZ workshop mods."); + finish("Updated " + successfulUpdates + "/" + steamWorkshopMods.size() + " Steam Workshop mods."); } } diff --git a/src/test/java/com/osiris/autoplug/client/tasks/updater/mods/DayZWorkshopModTest.java b/src/test/java/com/osiris/autoplug/client/tasks/updater/mods/SteamWorkshopModTest.java similarity index 91% rename from src/test/java/com/osiris/autoplug/client/tasks/updater/mods/DayZWorkshopModTest.java rename to src/test/java/com/osiris/autoplug/client/tasks/updater/mods/SteamWorkshopModTest.java index 75a74627..77a56c02 100644 --- a/src/test/java/com/osiris/autoplug/client/tasks/updater/mods/DayZWorkshopModTest.java +++ b/src/test/java/com/osiris/autoplug/client/tasks/updater/mods/SteamWorkshopModTest.java @@ -20,7 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -class DayZWorkshopModTest { +class SteamWorkshopModTest { @TempDir Path tempDir; @@ -35,7 +35,7 @@ void readsPublishedIdAndNameFromMetaCpp() throws Exception { "name = \"CF\";", "timestamp = 5249804932187309401;"); - DayZWorkshopMod mod = DayZWorkshopMod.readFromMeta(modDir, metaFile); + SteamWorkshopMod mod = SteamWorkshopMod.readFromMeta(modDir, metaFile); assertEquals(modDir, mod.getDirectory()); assertEquals("CF", mod.getName()); @@ -53,7 +53,7 @@ void findsModsWithMetaCppInDirectSubdirectories() throws Exception { writeMeta(firstModDir, "publishedid = 1559212036;", "name = \"CF\";"); writeMeta(secondModDir, "publishedid = 1564026768;", "name = \"Community-Online-Tools\";"); - List mods = DayZWorkshopMod.findIn(tempDir.toFile()); + List mods = SteamWorkshopMod.findIn(tempDir.toFile()); assertEquals(2, mods.size()); assertEquals("1559212036", mods.get(0).getPublishedId()); From 39f93ce1e7f9a9a13e0e0212c75209fe3acb55e4 Mon Sep 17 00:00:00 2001 From: JustABiologist Date: Fri, 15 May 2026 16:50:18 +0200 Subject: [PATCH 3/4] Integrate Steam Workshop mods into updater flow --- .../autoplug/client/configs/ModsConfig.java | 1 + .../tasks/updater/mods/SteamWorkshopMod.java | 16 +-- .../tasks/updater/mods/TaskModsUpdater.java | 111 ++++++++++-------- .../updater/mods/SteamWorkshopModTest.java | 95 +++++++++++++++ 4 files changed, 166 insertions(+), 57 deletions(-) diff --git a/src/main/java/com/osiris/autoplug/client/configs/ModsConfig.java b/src/main/java/com/osiris/autoplug/client/configs/ModsConfig.java index 6a9870ab..91cd45af 100644 --- a/src/main/java/com/osiris/autoplug/client/configs/ModsConfig.java +++ b/src/main/java/com/osiris/autoplug/client/configs/ModsConfig.java @@ -44,6 +44,7 @@ public ModsConfig() throws IOException, DuplicateKeyException, YamlReaderExcepti "If there are mods that weren't found by the search-algorithm, you can add an id (spigot or bukkit) and a custom link (optional & must be a static link to the latest mod jar).\n" + "modrinth-id: Is the 'Project-ID' and can be found on the mods modrinth site inside of the 'About' box, under 'Technical Information' at the bottom left.\n" + "curseforge-id: Is also called 'Project-ID' and can be found on the mods curseforge site inside of the 'About' box at the right.\n" + + "steam-workshop-id: The Steam Workshop item id read from the mods metadata. Requires server-updater.software to be a numeric Steam app-id.\n" + "ignore-content-type: If true, does not check if the downloaded file is of type jar or zip, and downloads it anyway.\n" + "force-latest: If true, does not search for updates compatible with this Minecraft version and simply picks the latest release.\n" + "force-update: If true, downloads the update every time even if its already on the latest version.\n" + diff --git a/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/SteamWorkshopMod.java b/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/SteamWorkshopMod.java index 559ab459..5510b49f 100644 --- a/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/SteamWorkshopMod.java +++ b/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/SteamWorkshopMod.java @@ -20,14 +20,13 @@ import java.util.Comparator; import java.util.List; -public class SteamWorkshopMod { +public class SteamWorkshopMod extends MinecraftMod { private final File directory; - private final String name; - private final String publishedId; + private String publishedId; public SteamWorkshopMod(File directory, String name, String publishedId) { + super(directory.getAbsolutePath(), name, publishedId, "Steam Workshop", null, null, null); this.directory = directory; - this.name = name; this.publishedId = publishedId; } @@ -35,14 +34,15 @@ public File getDirectory() { return directory; } - public String getName() { - return name; - } - public String getPublishedId() { return publishedId; } + public void setPublishedId(String publishedId) { + this.publishedId = publishedId; + setVersion(publishedId); + } + @NotNull public static List findIn(File dir) throws IOException { if (!dir.exists()) throw new FileNotFoundException("Directory does not exist: " + dir); diff --git a/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/TaskModsUpdater.java b/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/TaskModsUpdater.java index 01a97312..5ec1078f 100644 --- a/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/TaskModsUpdater.java +++ b/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/TaskModsUpdater.java @@ -85,13 +85,8 @@ public void runAtStart() throws Exception { userProfile = updaterConfig.mods_updater_profile.asString(); File modsDir = FileManager.convertRelativeToAbsolutePath(updaterConfig.mods_updater_path.asString()); - List steamWorkshopMods = SteamWorkshopMod.findIn(modsDir); - if (!steamWorkshopMods.isEmpty()) { - doSteamWorkshopUpdateLogic(steamWorkshopMods); - return; - } - this.allMods.addAll(new UtilsMinecraft().getMods(modsDir)); + this.allMods.addAll(SteamWorkshopMod.findIn(modsDir)); for (MinecraftMod installedMod : allMods) { @@ -106,6 +101,7 @@ public void runAtStart() throws Exception { YamlSection author = modsConfig.put(name, plName, "author").setDefValues(installedMod.getAuthor()); YamlSection modrinthId = modsConfig.put(name, plName, "modrinth-id"); YamlSection curseforgeId = modsConfig.put(name, plName, "curseforge-id"); + YamlSection steamWorkshopId = modsConfig.put(name, plName, "steam-workshop-id"); YamlSection ignoreContentType = modsConfig.put(name, plName, "ignore-content-type").setDefValues("false"); YamlSection forceLatest = modsConfig.put(name, plName, "force-latest").setDefValues("false"); YamlSection forceUpdate = modsConfig.put(name, plName, "force-update").setDefValues("false"); @@ -121,6 +117,8 @@ public void runAtStart() throws Exception { modrinthId.setValues(installedMod.modrinthId); if (installedMod.curseforgeId != null && curseforgeId.asString() == null) curseforgeId.setValues(installedMod.curseforgeId); + if (installedMod instanceof SteamWorkshopMod) + steamWorkshopId.setValues(((SteamWorkshopMod) installedMod).getPublishedId()); // Update the detailed mods in-memory values installedMod.modrinthId = (modrinthId.asString()); @@ -135,6 +133,14 @@ public void runAtStart() throws Exception { installedMod.jenkinsArtifactName = (jenkinsArtifactName.asString()); installedMod.jenkinsBuildId = (jenkinsBuildId.asInt()); installedMod.forceUpdate = forceUpdate.asBoolean(); + if (installedMod instanceof SteamWorkshopMod) { + ((SteamWorkshopMod) installedMod).setPublishedId(steamWorkshopId.asString()); + if (exclude.asBoolean()) + excludedMods.add(installedMod); + else + includedMods.add(installedMod); + continue; + } // Check for missing author in internal config if ((installedMod.getVersion() == null) @@ -194,11 +200,7 @@ public void runAtStart() throws Exception { int sizeBukkitMods = 0; int sizeUnknownMods = 0; int sizeCustomMods = 0; - - - String mcVersion = updaterConfig.mods_updater_version.asString(); - if (mcVersion == null) updaterConfig.server_updater_version.asString(); - if (mcVersion == null) mcVersion = Server.getMCVersion(); + int sizeSteamWorkshopMods = 0; ExecutorService executorService; if (updaterConfig.mods_updater_async.asBoolean()) @@ -207,11 +209,15 @@ public void runAtStart() throws Exception { executorService = Executors.newSingleThreadExecutor(); InstalledModLoader modLoader = new InstalledModLoader(); List> activeFutures = new ArrayList<>(); + String mcVersion = null; for (MinecraftMod mod : includedMods) { try { setStatus("Initialising update check for " + mod.getName() + "..."); - if (mod.customCheckURL != null) { // Custom Check + if (mod instanceof SteamWorkshopMod) { + sizeSteamWorkshopMods++; + activeFutures.add(executorService.submit(() -> doSteamWorkshopUpdateLogic((SteamWorkshopMod) mod))); + } else if (mod.customCheckURL != null) { // Custom Check sizeCustomMods++; activeFutures.add(executorService.submit(() -> new ResourceFinder().findByCustomCheckURL(mod))); } else if (mod.jenkinsProjectUrl != null) { // JENKINS MOD @@ -223,6 +229,11 @@ public void runAtStart() throws Exception { } else { sizeUnknownMods++; // MODRINTH OR CURSEFORGE MOD mod.ignoreContentType = true; // TODO temporary workaround for xamazon-json content type curseforge/bukkit issue: https://github.com/Osiris-Team/AutoPlug-Client/issues/109 + if (mcVersion == null) { + mcVersion = updaterConfig.mods_updater_version.asString(); + if (mcVersion == null) mcVersion = updaterConfig.server_updater_version.asString(); + if (mcVersion == null) mcVersion = Server.getMCVersion(); + } String finalMcVersion = mcVersion; activeFutures.add(executorService.submit(() -> new ResourceFinder().findByModrinthOrCurseforge(modLoader, mod, finalMcVersion, updaterConfig.mods_update_check_name_for_mod_loader.asBoolean()))); } @@ -255,7 +266,13 @@ public void runAtStart() throws Exception { String resultModrinthId = mod.modrinthId; String resultCurseForgeId = mod.curseforgeId; this.setStatus("Checked '" + mod.getName() + "' mod (" + results.size() + "/" + includedSize + ")"); - if (code == SearchResult.Type.UP_TO_DATE || code == SearchResult.Type.UPDATE_AVAILABLE) { + if (mod instanceof SteamWorkshopMod) { + if (code == SearchResult.Type.API_ERROR) + if (result.getException() != null) + getWarnings().add(new BWarning(this, result.getException(), "There was a Steam Workshop update error for " + mod.getName() + "!")); + else + getWarnings().add(new BWarning(this, new Exception("There was a Steam Workshop update error for " + mod.getName() + "!"))); + } else if (code == SearchResult.Type.UP_TO_DATE || code == SearchResult.Type.UPDATE_AVAILABLE) { doDownloadLogic(mod, result); } else if (code == SearchResult.Type.API_ERROR) if (result.getException() != null) @@ -347,60 +364,56 @@ else if (code == SearchResult.Type.RESOURCE_NOT_FOUND) } - private void doSteamWorkshopUpdateLogic(@NotNull List steamWorkshopMods) throws Exception { - setMax(steamWorkshopMods.size()); + private SearchResult doSteamWorkshopUpdateLogic(@NotNull SteamWorkshopMod mod) { + SearchResult result = new SearchResult(null, SearchResult.Type.UP_TO_DATE, mod.getPublishedId(), null, "steam-workshop", null, null, false); + result.mod = mod; String workshopAppId = updaterConfig.server_software.asString(); if (workshopAppId == null || !workshopAppId.matches("\\d+")) { - setSuccess(false); - addWarning("Steam Workshop mods were found, but server-updater.software is not a numeric Steam app-id."); - finish("Found " + steamWorkshopMods.size() + " Steam Workshop mods, but no Steam app-id is configured."); - return; + result.type = SearchResult.Type.API_ERROR; + result.setException(new Exception("Steam Workshop mod '" + mod.getName() + "' was found, but server-updater.software is not a numeric Steam app-id.")); + return result; } if (userProfile.equals(notifyProfile)) { - for (SteamWorkshopMod mod : steamWorkshopMods) - addInfo("NOTIFY: Steam Workshop mod '" + mod.getName() + "' can be updated with Workshop item " + mod.getPublishedId() + " for app " + workshopAppId + "."); - finish("Found " + steamWorkshopMods.size() + " Steam Workshop mods."); - return; + addInfo("NOTIFY: Steam Workshop mod '" + mod.getName() + "' can be updated with Workshop item " + mod.getPublishedId() + " for app " + workshopAppId + "."); + result.type = SearchResult.Type.UPDATE_AVAILABLE; + return result; } - SteamCMD steamCMD = new SteamCMD(); - int successfulUpdates = 0; - int checkedMods = 0; - for (SteamWorkshopMod mod : steamWorkshopMods) { - checkedMods++; - setStatus("Updating Steam Workshop mod '" + mod.getName() + "' (" + checkedMods + "/" + steamWorkshopMods.size() + ")..."); - boolean isSuccess = steamCMD.installOrUpdateWorkshopItem(workshopAppId, mod.getPublishedId(), line -> { - AL.debug(this.getClass(), "SteamCMD-Out: " + line); - setStatus(line); - }, errLine -> { - AL.debug(this.getClass(), "SteamCMD-Err-Out: " + errLine); - setStatus(errLine); - addWarning(errLine); - }); - if (!isSuccess) { - addWarning("Failed to update Steam Workshop mod '" + mod.getName() + "' via SteamCMD."); - continue; - } + try { + SteamCMD steamCMD = createSteamCMD(); + setStatus("Updating Steam Workshop mod '" + mod.getName() + "'..."); + boolean isSuccess = steamCMD.installOrUpdateWorkshopItem(workshopAppId, mod.getPublishedId(), + line -> { + AL.debug(this.getClass(), "SteamCMD-Out: " + line); + setStatus(line); + }, errLine -> { + AL.debug(this.getClass(), "SteamCMD-Err-Out: " + errLine); + setStatus(errLine); + addWarning(errLine); + }); + if (!isSuccess) + throw new Exception("Failed to update Steam Workshop mod '" + mod.getName() + "' via SteamCMD."); File downloadedDir = steamCMD.getWorkshopItemDir(workshopAppId, mod.getPublishedId()); if (userProfile.equals(manualProfile)) { addInfo("MANUAL: Downloaded Steam Workshop mod '" + mod.getName() + "' to " + downloadedDir.getAbsolutePath() + "."); + result.type = SearchResult.Type.UPDATE_DOWNLOADED; } else { setStatus("Copying Steam Workshop mod '" + mod.getName() + "' into " + mod.getDirectory().getAbsolutePath() + "..."); FileUtils.copyDirectory(downloadedDir, mod.getDirectory()); addInfo("Updated Steam Workshop mod '" + mod.getName() + "' from Workshop item " + mod.getPublishedId() + "."); + result.type = SearchResult.Type.UPDATE_INSTALLED; } - successfulUpdates++; + } catch (Exception e) { + result.type = SearchResult.Type.API_ERROR; + result.setException(e); } + return result; + } - if (successfulUpdates == steamWorkshopMods.size()) { - setSuccess(true); - finish("Updated " + successfulUpdates + " Steam Workshop mods."); - } else { - setSuccess(false); - finish("Updated " + successfulUpdates + "/" + steamWorkshopMods.size() + " Steam Workshop mods."); - } + SteamCMD createSteamCMD() { + return new SteamCMD(); } private void doDownloadLogic(@NotNull MinecraftMod mod, SearchResult result) { diff --git a/src/test/java/com/osiris/autoplug/client/tasks/updater/mods/SteamWorkshopModTest.java b/src/test/java/com/osiris/autoplug/client/tasks/updater/mods/SteamWorkshopModTest.java index 77a56c02..6a651220 100644 --- a/src/test/java/com/osiris/autoplug/client/tasks/updater/mods/SteamWorkshopModTest.java +++ b/src/test/java/com/osiris/autoplug/client/tasks/updater/mods/SteamWorkshopModTest.java @@ -8,17 +8,26 @@ package com.osiris.autoplug.client.tasks.updater.mods; +import com.osiris.autoplug.client.configs.ModsConfig; +import com.osiris.autoplug.client.configs.UpdaterConfig; +import com.osiris.autoplug.client.utils.GD; +import com.osiris.autoplug.client.utils.SteamCMD; +import com.osiris.betterthread.BThreadManager; +import com.osiris.jlib.logger.AL; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import java.io.File; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.List; +import java.util.function.Consumer; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; class SteamWorkshopModTest { @@ -60,9 +69,95 @@ void findsModsWithMetaCppInDirectSubdirectories() throws Exception { assertEquals("1564026768", mods.get(1).getPublishedId()); } + @Test + void taskUpdatesWorkshopModThroughSteamCmdAndCachesMetadata() throws Exception { + File oldWorkingDir = GD.WORKING_DIR; + File oldDownloadsDir = GD.DOWNLOADS_DIR; + String oldUserDir = System.getProperty("user.dir"); + useWorkingDir(tempDir); + try { + File modDir = tempDir.resolve("mods/@CF").toFile(); + modDir.mkdirs(); + writeMeta(modDir, "publishedid = 1559212036;", "name = \"CF\";"); + Files.write(modDir.toPath().resolve("old.txt"), Arrays.asList("old"), StandardCharsets.UTF_8); + + File downloadedDir = tempDir.resolve("steamcmd/steamapps/workshop/content/221100/1559212036").toFile(); + downloadedDir.mkdirs(); + Files.write(downloadedDir.toPath().resolve("updated.txt"), Arrays.asList("updated"), StandardCharsets.UTF_8); + + UpdaterConfig updaterConfig = new UpdaterConfig(); + updaterConfig.mods_updater.setValues("true"); + updaterConfig.mods_updater_profile.setValues("AUTOMATIC"); + updaterConfig.mods_updater_path.setValues("./mods"); + updaterConfig.mods_updater_async.setValues("false"); + updaterConfig.server_software.setValues("221100"); + updaterConfig.save(); + + FakeSteamCMD steamCMD = new FakeSteamCMD(downloadedDir); + TaskModsUpdater task = new TaskModsUpdater("ModsUpdater", new BThreadManager()) { + @Override + SteamCMD createSteamCMD() { + return steamCMD; + } + }; + + task.runAtStart(); + + assertEquals(1, steamCMD.updateCalls); + assertEquals("221100", steamCMD.workshopAppId); + assertEquals("1559212036", steamCMD.workshopItemId); + assertEquals("updated", Files.readAllLines(modDir.toPath().resolve("updated.txt"), StandardCharsets.UTF_8).get(0)); + assertTrue(task.getWarnings().isEmpty()); + + ModsConfig modsConfig = new ModsConfig(); + modsConfig.load(); + assertEquals("1559212036", modsConfig.get("mods", "CF", "steam-workshop-id").asString()); + } finally { + System.setProperty("user.dir", oldUserDir); + GD.WORKING_DIR = oldWorkingDir; + GD.DOWNLOADS_DIR = oldDownloadsDir; + } + } + private File writeMeta(File modDir, String... lines) throws Exception { File metaFile = new File(modDir, "meta.cpp"); Files.write(metaFile.toPath(), Arrays.asList(lines), StandardCharsets.UTF_8); return metaFile; } + + private void useWorkingDir(Path dir) throws IOException { + System.setProperty("user.dir", dir.toAbsolutePath().toString()); + GD.VERSION = "AutoPlug-Client Test-Version"; + GD.WORKING_DIR = dir.toFile(); + GD.DOWNLOADS_DIR = dir.resolve("autoplug/downloads").toFile(); + GD.DOWNLOADS_DIR.mkdirs(); + File logFile = dir.resolve("autoplug/logs/latest.log").toFile(); + logFile.getParentFile().mkdirs(); + new AL().start("AL", true, logFile, false, false); + } + + private static class FakeSteamCMD extends SteamCMD { + final File workshopItemDir; + int updateCalls; + String workshopAppId; + String workshopItemId; + + FakeSteamCMD(File workshopItemDir) { + this.workshopItemDir = workshopItemDir; + } + + @Override + public boolean installOrUpdateWorkshopItem(String workshopAppId, String workshopItemId, Consumer onLog, Consumer onLogErr) { + this.updateCalls++; + this.workshopAppId = workshopAppId; + this.workshopItemId = workshopItemId; + onLog.accept("Success. Downloaded item " + workshopItemId); + return true; + } + + @Override + public File getWorkshopItemDir(String workshopAppId, String workshopItemId) { + return workshopItemDir; + } + } } From 2c6f7350b27951d62648298fcdd7e78fccf5e5d7 Mon Sep 17 00:00:00 2001 From: JustABiologist Date: Sat, 16 May 2026 17:49:11 +0200 Subject: [PATCH 4/4] Check Steam Workshop metadata before updating mods --- .../tasks/updater/mods/ModDownloadTask.java | 25 ++++ .../tasks/updater/mods/SteamWorkshopMod.java | 19 ++- .../tasks/updater/mods/TaskModDownload.java | 6 +- .../tasks/updater/mods/TaskModsUpdater.java | 126 ++++++++++-------- .../mods/TaskSteamWorkshopModDownload.java | 91 +++++++++++++ .../autoplug/client/utils/SteamCMD.java | 91 +++++++++++++ .../updater/mods/SteamWorkshopModTest.java | 97 +++++++++++--- 7 files changed, 377 insertions(+), 78 deletions(-) create mode 100644 src/main/java/com/osiris/autoplug/client/tasks/updater/mods/ModDownloadTask.java create mode 100644 src/main/java/com/osiris/autoplug/client/tasks/updater/mods/TaskSteamWorkshopModDownload.java diff --git a/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/ModDownloadTask.java b/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/ModDownloadTask.java new file mode 100644 index 00000000..6e349cad --- /dev/null +++ b/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/ModDownloadTask.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 Osiris-Team. + * All rights reserved. + * + * This software is copyrighted work, licensed under the terms + * of the MIT-License. Consult the "LICENSE" file for details. + */ + +package com.osiris.autoplug.client.tasks.updater.mods; + +import com.osiris.autoplug.client.tasks.updater.search.SearchResult; + +interface ModDownloadTask { + void start(); + + boolean isAlive(); + + String getPlName(); + + SearchResult getSearchResult(); + + boolean isDownloadSuccessful(); + + boolean isInstallSuccessful(); +} diff --git a/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/SteamWorkshopMod.java b/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/SteamWorkshopMod.java index 5510b49f..39e90ba2 100644 --- a/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/SteamWorkshopMod.java +++ b/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/SteamWorkshopMod.java @@ -23,11 +23,17 @@ public class SteamWorkshopMod extends MinecraftMod { private final File directory; private String publishedId; + private final String timestamp; public SteamWorkshopMod(File directory, String name, String publishedId) { - super(directory.getAbsolutePath(), name, publishedId, "Steam Workshop", null, null, null); + this(directory, name, publishedId, null); + } + + public SteamWorkshopMod(File directory, String name, String publishedId, String timestamp) { + super(directory.getAbsolutePath(), name, timestamp != null ? timestamp : publishedId, "Steam Workshop", null, null, null); this.directory = directory; this.publishedId = publishedId; + this.timestamp = timestamp; } public File getDirectory() { @@ -40,7 +46,12 @@ public String getPublishedId() { public void setPublishedId(String publishedId) { this.publishedId = publishedId; - setVersion(publishedId); + if (getVersion() == null) + setVersion(publishedId); + } + + public String getTimestamp() { + return timestamp; } @NotNull @@ -62,6 +73,7 @@ public static List findIn(File dir) throws IOException { static SteamWorkshopMod readFromMeta(File modDir, File metaFile) throws IOException { String name = modDir.getName(); String publishedId = null; + String timestamp = null; for (String line : Files.readAllLines(metaFile.toPath(), StandardCharsets.UTF_8)) { String trimmedLine = line.trim(); if (trimmedLine.isEmpty() || trimmedLine.startsWith("//")) continue; @@ -79,11 +91,12 @@ static SteamWorkshopMod readFromMeta(File modDir, File metaFile) throws IOExcept if (key.equals("name")) name = value; if (key.equals("publishedid")) publishedId = value; + if (key.equals("timestamp")) timestamp = value; } if (publishedId == null || !publishedId.matches("\\d+")) throw new IOException("Failed to read publishedid from " + metaFile); - return new SteamWorkshopMod(modDir, name, publishedId); + return new SteamWorkshopMod(modDir, name, publishedId, timestamp); } } diff --git a/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/TaskModDownload.java b/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/TaskModDownload.java index cb99e5a8..77b3e2bd 100644 --- a/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/TaskModDownload.java +++ b/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/TaskModDownload.java @@ -25,7 +25,7 @@ import java.io.FileOutputStream; -public class TaskModDownload extends BThread { +public class TaskModDownload extends BThread implements ModDownloadTask { private final String plName; private final String plLatestVersion; private final String url; @@ -207,6 +207,10 @@ public File getDownloadDest() { return dest; } + public SearchResult getSearchResult() { + return searchResult; + } + public boolean isDownloadSuccessful() { return isDownloadSuccessful; } diff --git a/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/TaskModsUpdater.java b/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/TaskModsUpdater.java index 5ec1078f..d540fefa 100644 --- a/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/TaskModsUpdater.java +++ b/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/TaskModsUpdater.java @@ -23,8 +23,6 @@ import com.osiris.betterthread.BWarning; import com.osiris.dyml.YamlSection; import com.osiris.dyml.exceptions.DuplicateKeyException; -import com.osiris.jlib.logger.AL; -import org.apache.commons.io.FileUtils; import org.jetbrains.annotations.NotNull; import java.io.DataInputStream; @@ -43,7 +41,7 @@ public class TaskModsUpdater extends BThread { private final String manualProfile = "MANUAL"; private final String automaticProfile = "AUTOMATIC"; private final int updatesDownloaded = 0; - private final List downloadTasksList = new ArrayList<>(); + private final List downloadTasksList = new ArrayList<>(); @NotNull private final List includedMods = new ArrayList<>(); @NotNull @@ -135,6 +133,7 @@ public void runAtStart() throws Exception { installedMod.forceUpdate = forceUpdate.asBoolean(); if (installedMod instanceof SteamWorkshopMod) { ((SteamWorkshopMod) installedMod).setPublishedId(steamWorkshopId.asString()); + installedMod.setVersion(version.asString()); if (exclude.asBoolean()) excludedMods.add(installedMod); else @@ -202,6 +201,10 @@ public void runAtStart() throws Exception { int sizeCustomMods = 0; int sizeSteamWorkshopMods = 0; + String mcVersion = updaterConfig.mods_updater_version.asString(); + if (mcVersion == null) mcVersion = updaterConfig.server_updater_version.asString(); + if (mcVersion == null) mcVersion = Server.getMCVersion(); + ExecutorService executorService; if (updaterConfig.mods_updater_async.asBoolean()) executorService = Executors.newFixedThreadPool(includedSize); @@ -209,14 +212,13 @@ public void runAtStart() throws Exception { executorService = Executors.newSingleThreadExecutor(); InstalledModLoader modLoader = new InstalledModLoader(); List> activeFutures = new ArrayList<>(); - String mcVersion = null; for (MinecraftMod mod : includedMods) { try { setStatus("Initialising update check for " + mod.getName() + "..."); if (mod instanceof SteamWorkshopMod) { sizeSteamWorkshopMods++; - activeFutures.add(executorService.submit(() -> doSteamWorkshopUpdateLogic((SteamWorkshopMod) mod))); + activeFutures.add(executorService.submit(() -> findSteamWorkshopUpdate((SteamWorkshopMod) mod))); } else if (mod.customCheckURL != null) { // Custom Check sizeCustomMods++; activeFutures.add(executorService.submit(() -> new ResourceFinder().findByCustomCheckURL(mod))); @@ -229,11 +231,6 @@ public void runAtStart() throws Exception { } else { sizeUnknownMods++; // MODRINTH OR CURSEFORGE MOD mod.ignoreContentType = true; // TODO temporary workaround for xamazon-json content type curseforge/bukkit issue: https://github.com/Osiris-Team/AutoPlug-Client/issues/109 - if (mcVersion == null) { - mcVersion = updaterConfig.mods_updater_version.asString(); - if (mcVersion == null) mcVersion = updaterConfig.server_updater_version.asString(); - if (mcVersion == null) mcVersion = Server.getMCVersion(); - } String finalMcVersion = mcVersion; activeFutures.add(executorService.submit(() -> new ResourceFinder().findByModrinthOrCurseforge(modLoader, mod, finalMcVersion, updaterConfig.mods_update_check_name_for_mod_loader.asBoolean()))); } @@ -266,13 +263,7 @@ public void runAtStart() throws Exception { String resultModrinthId = mod.modrinthId; String resultCurseForgeId = mod.curseforgeId; this.setStatus("Checked '" + mod.getName() + "' mod (" + results.size() + "/" + includedSize + ")"); - if (mod instanceof SteamWorkshopMod) { - if (code == SearchResult.Type.API_ERROR) - if (result.getException() != null) - getWarnings().add(new BWarning(this, result.getException(), "There was a Steam Workshop update error for " + mod.getName() + "!")); - else - getWarnings().add(new BWarning(this, new Exception("There was a Steam Workshop update error for " + mod.getName() + "!"))); - } else if (code == SearchResult.Type.UP_TO_DATE || code == SearchResult.Type.UPDATE_AVAILABLE) { + if (code == SearchResult.Type.UP_TO_DATE || code == SearchResult.Type.UPDATE_AVAILABLE) { doDownloadLogic(mod, result); } else if (code == SearchResult.Type.API_ERROR) if (result.getException() != null) @@ -299,12 +290,13 @@ else if (code == SearchResult.Type.RESOURCE_NOT_FOUND) } } } + executorService.shutdown(); // Wait until all download tasks have finished. while (!downloadTasksList.isEmpty()) { Thread.sleep(1000); - TaskModDownload download = null; - for (TaskModDownload task : + ModDownloadTask download = null; + for (ModDownloadTask task : downloadTasksList) { if (!task.isAlive()) { download = task; @@ -332,10 +324,10 @@ else if (code == SearchResult.Type.RESOURCE_NOT_FOUND) matchingResult.type = SearchResult.Type.UPDATE_INSTALLED; YamlSection jenkinsBuildId = modsConfig.get( modsConfigName, download.getPlName(), "alternatives", "jenkins", "build-id"); - jenkinsBuildId.setValues(String.valueOf(download.searchResult.jenkinsId)); + jenkinsBuildId.setValues(String.valueOf(download.getSearchResult().jenkinsId)); YamlSection version = modsConfig.get( modsConfigName, download.getPlName(), "version"); - version.setValues(download.searchResult.getLatestVersion()); + version.setValues(download.getSearchResult().getLatestVersion()); } } @@ -364,47 +356,22 @@ else if (code == SearchResult.Type.RESOURCE_NOT_FOUND) } - private SearchResult doSteamWorkshopUpdateLogic(@NotNull SteamWorkshopMod mod) { - SearchResult result = new SearchResult(null, SearchResult.Type.UP_TO_DATE, mod.getPublishedId(), null, "steam-workshop", null, null, false); + private SearchResult findSteamWorkshopUpdate(@NotNull SteamWorkshopMod mod) { + SearchResult result = new SearchResult(null, SearchResult.Type.UP_TO_DATE, mod.getVersion(), null, "steam-workshop", null, null, false); result.mod = mod; - String workshopAppId = updaterConfig.server_software.asString(); - if (workshopAppId == null || !workshopAppId.matches("\\d+")) { + String workshopAppId = getWorkshopAppId(); + if (workshopAppId == null) { result.type = SearchResult.Type.API_ERROR; result.setException(new Exception("Steam Workshop mod '" + mod.getName() + "' was found, but server-updater.software is not a numeric Steam app-id.")); return result; } - if (userProfile.equals(notifyProfile)) { - addInfo("NOTIFY: Steam Workshop mod '" + mod.getName() + "' can be updated with Workshop item " + mod.getPublishedId() + " for app " + workshopAppId + "."); - result.type = SearchResult.Type.UPDATE_AVAILABLE; - return result; - } - try { - SteamCMD steamCMD = createSteamCMD(); - setStatus("Updating Steam Workshop mod '" + mod.getName() + "'..."); - boolean isSuccess = steamCMD.installOrUpdateWorkshopItem(workshopAppId, mod.getPublishedId(), - line -> { - AL.debug(this.getClass(), "SteamCMD-Out: " + line); - setStatus(line); - }, errLine -> { - AL.debug(this.getClass(), "SteamCMD-Err-Out: " + errLine); - setStatus(errLine); - addWarning(errLine); - }); - if (!isSuccess) - throw new Exception("Failed to update Steam Workshop mod '" + mod.getName() + "' via SteamCMD."); - - File downloadedDir = steamCMD.getWorkshopItemDir(workshopAppId, mod.getPublishedId()); - if (userProfile.equals(manualProfile)) { - addInfo("MANUAL: Downloaded Steam Workshop mod '" + mod.getName() + "' to " + downloadedDir.getAbsolutePath() + "."); - result.type = SearchResult.Type.UPDATE_DOWNLOADED; - } else { - setStatus("Copying Steam Workshop mod '" + mod.getName() + "' into " + mod.getDirectory().getAbsolutePath() + "..."); - FileUtils.copyDirectory(downloadedDir, mod.getDirectory()); - addInfo("Updated Steam Workshop mod '" + mod.getName() + "' from Workshop item " + mod.getPublishedId() + "."); - result.type = SearchResult.Type.UPDATE_INSTALLED; - } + SteamCMD.SteamWorkshopItemDetails details = createSteamCMD().getWorkshopItemDetails(mod.getPublishedId()); + result.latestVersion = details.getTimeUpdated(); + result.downloadUrl = details.getFileUrl(); + if (hasSteamWorkshopUpdate(mod, details.getTimeUpdated())) + result.type = SearchResult.Type.UPDATE_AVAILABLE; } catch (Exception e) { result.type = SearchResult.Type.API_ERROR; result.setException(e); @@ -412,6 +379,36 @@ private SearchResult doSteamWorkshopUpdateLogic(@NotNull SteamWorkshopMod mod) { return result; } + private String getWorkshopAppId() { + String workshopAppId = updaterConfig.server_software.asString(); + if (workshopAppId == null || !workshopAppId.matches("\\d+")) + return null; + return workshopAppId; + } + + private boolean hasSteamWorkshopUpdate(SteamWorkshopMod mod, String latestVersion) { + if (latestVersion == null || latestVersion.isEmpty()) + return false; + String currentVersion = mod.getVersion(); + if (currentVersion == null || currentVersion.isEmpty()) + return true; + if (latestVersion.equals(currentVersion)) + return false; + if (currentVersion.equals(mod.getPublishedId())) + return true; + if (isSteamUnixTimestamp(latestVersion) && !isSteamUnixTimestamp(currentVersion)) + return true; + try { + return Long.parseLong(latestVersion) > Long.parseLong(currentVersion); + } catch (NumberFormatException e) { + return true; + } + } + + private boolean isSteamUnixTimestamp(String version) { + return version != null && version.matches("\\d{1,10}"); + } + SteamCMD createSteamCMD() { return new SteamCMD(); } @@ -440,8 +437,25 @@ private void doDownloadLogic(@NotNull MinecraftMod mod, SearchResult result) { } if (userProfile.equals(notifyProfile)) { - addInfo("NOTIFY: Mod '" + mod.getName() + "' has an update available (" + mod.getVersion() + " -> " + latest + "). Download url: " + downloadUrl); + if (mod instanceof SteamWorkshopMod) + addInfo("NOTIFY: Steam Workshop mod '" + mod.getName() + "' has an update available (" + mod.getVersion() + " -> " + latest + ")."); + else + addInfo("NOTIFY: Mod '" + mod.getName() + "' has an update available (" + mod.getVersion() + " -> " + latest + "). Download url: " + downloadUrl); } else { + if (mod instanceof SteamWorkshopMod) { + String workshopAppId = getWorkshopAppId(); + if (workshopAppId == null) { + getWarnings().add(new BWarning(this, new Exception("Steam Workshop mod '" + mod.getName() + "' was found, but server-updater.software is not a numeric Steam app-id."))); + return; + } + + TaskSteamWorkshopModDownload task = new TaskSteamWorkshopModDownload("SteamWorkshopModDownloader", getManager(), + (SteamWorkshopMod) mod, workshopAppId, userProfile, createSteamCMD(), result); + downloadTasksList.add(task); + task.start(); + return; + } + // Make sure that plName and plLatestVersion do not contain any slashes (/ or \) that could break the file name UtilsFile utilsFile = new UtilsFile(); mod.setName(utilsFile.getValidFileName(mod.getName())); diff --git a/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/TaskSteamWorkshopModDownload.java b/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/TaskSteamWorkshopModDownload.java new file mode 100644 index 00000000..0a89b311 --- /dev/null +++ b/src/main/java/com/osiris/autoplug/client/tasks/updater/mods/TaskSteamWorkshopModDownload.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2024 Osiris-Team. + * All rights reserved. + * + * This software is copyrighted work, licensed under the terms + * of the MIT-License. Consult the "LICENSE" file for details. + */ + +package com.osiris.autoplug.client.tasks.updater.mods; + +import com.osiris.autoplug.client.tasks.updater.search.SearchResult; +import com.osiris.autoplug.client.utils.SteamCMD; +import com.osiris.betterthread.BThread; +import com.osiris.betterthread.BThreadManager; +import com.osiris.jlib.logger.AL; +import org.apache.commons.io.FileUtils; + +import java.io.File; + +public class TaskSteamWorkshopModDownload extends BThread implements ModDownloadTask { + private final SteamWorkshopMod mod; + private final String workshopAppId; + private final String profile; + private final SteamCMD steamCMD; + private final SearchResult searchResult; + private boolean isDownloadSuccessful; + private boolean isInstallSuccessful; + + public TaskSteamWorkshopModDownload(String name, BThreadManager manager, SteamWorkshopMod mod, + String workshopAppId, String profile, SteamCMD steamCMD, + SearchResult searchResult) { + super(name, manager); + this.mod = mod; + this.workshopAppId = workshopAppId; + this.profile = profile; + this.steamCMD = steamCMD; + this.searchResult = searchResult; + } + + @Override + public void runAtStart() throws Exception { + super.runAtStart(); + + if (profile.equals("NOTIFY")) { + setStatus("Your profile doesn't allow downloads! Profile: " + profile); + finish(false); + return; + } + + setStatus("Downloading Steam Workshop mod " + mod.getName() + "..."); + boolean isSuccess = steamCMD.installOrUpdateWorkshopItem(workshopAppId, mod.getPublishedId(), + line -> { + AL.debug(this.getClass(), "SteamCMD-Out: " + line); + setStatus(line); + }, errLine -> { + AL.debug(this.getClass(), "SteamCMD-Err-Out: " + errLine); + setStatus(errLine); + addWarning(errLine); + }); + if (!isSuccess) + throw new Exception("Failed to update Steam Workshop mod '" + mod.getName() + "' via SteamCMD."); + + isDownloadSuccessful = true; + File downloadedDir = steamCMD.getWorkshopItemDir(workshopAppId, mod.getPublishedId()); + if (profile.equals("MANUAL")) { + setStatus("Downloaded Steam Workshop mod " + mod.getName() + " to " + downloadedDir.getAbsolutePath()); + return; + } + + setStatus("Installing Steam Workshop mod " + mod.getName() + "..."); + FileUtils.copyDirectory(downloadedDir, mod.getDirectory()); + isInstallSuccessful = true; + setStatus("Installed update for " + mod.getName() + " successfully!"); + } + + public String getPlName() { + return mod.getName(); + } + + public SearchResult getSearchResult() { + return searchResult; + } + + public boolean isDownloadSuccessful() { + return isDownloadSuccessful; + } + + public boolean isInstallSuccessful() { + return isInstallSuccessful; + } +} diff --git a/src/main/java/com/osiris/autoplug/client/utils/SteamCMD.java b/src/main/java/com/osiris/autoplug/client/utils/SteamCMD.java index 6b0f8dfa..e4cb0b2c 100644 --- a/src/main/java/com/osiris/autoplug/client/utils/SteamCMD.java +++ b/src/main/java/com/osiris/autoplug/client/utils/SteamCMD.java @@ -8,6 +8,9 @@ package com.osiris.autoplug.client.utils; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import com.osiris.autoplug.client.configs.UpdaterConfig; import com.osiris.autoplug.client.tasks.updater.TaskDownload; import com.osiris.autoplug.client.utils.io.AsyncReader; @@ -15,6 +18,11 @@ import com.osiris.betterthread.BThreadManager; import com.osiris.dyml.exceptions.*; import com.osiris.jlib.logger.AL; +import okhttp3.FormBody; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; import org.rauschig.jarchivelib.ArchiveFormat; import org.rauschig.jarchivelib.Archiver; import org.rauschig.jarchivelib.ArchiverFactory; @@ -35,6 +43,7 @@ @SuppressWarnings({"WeakerAccess", "unused"}) public class SteamCMD { + private static final String STEAM_WORKSHOP_DETAILS_URL = "https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/"; private static final String STEAMCMD_WORKSHOP_COMMAND = "+login {LOGIN} +workshop_download_item {WORKSHOP_APP} {WORKSHOP_ITEM} validate +quit"; private final String steamcmdArchive = "steamcmd" + (isWindows ? ".zip" : isMac ? "_osx.tar.gz" : "_linux.tar.gz"); private final String steamcmdExtension = isWindows ? ".exe" : ".sh"; @@ -197,6 +206,52 @@ public boolean installOrUpdateWorkshopItem(String workshopAppId, String workshop } } + public SteamWorkshopItemDetails getWorkshopItemDetails(String workshopItemId) throws IOException { + Request request = new Request.Builder() + .url(STEAM_WORKSHOP_DETAILS_URL) + .post(new FormBody.Builder() + .add("itemcount", "1") + .add("publishedfileids[0]", workshopItemId) + .build()) + .header("User-Agent", "AutoPlug-Client - https://autoplug.one") + .build(); + + Response response = new OkHttpClient().newCall(request).execute(); + ResponseBody body = null; + try { + if (response.code() != 200) + throw new IOException("Steam Workshop details request failed for item " + workshopItemId + " with code " + response.code() + " message: " + response.message()); + + body = response.body(); + if (body == null) + throw new IOException("Steam Workshop details request returned no body for item " + workshopItemId); + + JsonObject root = JsonParser.parseString(body.string()).getAsJsonObject(); + JsonObject responseObject = root.getAsJsonObject("response"); + JsonArray details = responseObject == null ? null : responseObject.getAsJsonArray("publishedfiledetails"); + if (details == null || details.size() == 0) + throw new IOException("Steam Workshop details request returned no details for item " + workshopItemId); + + JsonObject detail = details.get(0).getAsJsonObject(); + int result = detail.has("result") ? detail.get("result").getAsInt() : 0; + if (result != 1) + throw new IOException("Steam Workshop details request failed for item " + workshopItemId + " with result " + result); + + String timeUpdated = getString(detail, "time_updated"); + if (timeUpdated == null || timeUpdated.isEmpty()) + throw new IOException("Steam Workshop details for item " + workshopItemId + " did not contain time_updated."); + + return new SteamWorkshopItemDetails( + getString(detail, "publishedfileid"), + getString(detail, "title"), + timeUpdated, + getString(detail, "file_url")); + } finally { + if (body != null) body.close(); + response.close(); + } + } + public File getWorkshopItemDir(String workshopAppId, String workshopItemId) { return new File(destDir + "/steamapps/workshop/content/" + workshopAppId + "/" + workshopItemId); } @@ -214,10 +269,46 @@ private String getLogin() throws NotLoadedException, YamlReaderException, YamlWr return login; } + private static String getString(JsonObject object, String key) { + if (object == null || !object.has(key) || object.get(key).isJsonNull()) + return null; + return object.get(key).getAsString(); + } + public String getResolutionForError(String error) { for (Map.Entry entry : errorResolutions.entrySet()) if (error.contains(entry.getKey())) return entry.getValue(); return "Unknown. :("; } + public static class SteamWorkshopItemDetails { + private final String publishedFileId; + private final String title; + private final String timeUpdated; + private final String fileUrl; + + public SteamWorkshopItemDetails(String publishedFileId, String title, String timeUpdated, String fileUrl) { + this.publishedFileId = publishedFileId; + this.title = title; + this.timeUpdated = timeUpdated; + this.fileUrl = fileUrl; + } + + public String getPublishedFileId() { + return publishedFileId; + } + + public String getTitle() { + return title; + } + + public String getTimeUpdated() { + return timeUpdated; + } + + public String getFileUrl() { + return fileUrl; + } + } + } diff --git a/src/test/java/com/osiris/autoplug/client/tasks/updater/mods/SteamWorkshopModTest.java b/src/test/java/com/osiris/autoplug/client/tasks/updater/mods/SteamWorkshopModTest.java index 6a651220..b14782d0 100644 --- a/src/test/java/com/osiris/autoplug/client/tasks/updater/mods/SteamWorkshopModTest.java +++ b/src/test/java/com/osiris/autoplug/client/tasks/updater/mods/SteamWorkshopModTest.java @@ -27,6 +27,7 @@ import java.util.function.Consumer; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; class SteamWorkshopModTest { @@ -49,6 +50,7 @@ void readsPublishedIdAndNameFromMetaCpp() throws Exception { assertEquals(modDir, mod.getDirectory()); assertEquals("CF", mod.getName()); assertEquals("1559212036", mod.getPublishedId()); + assertEquals("5249804932187309401", mod.getTimestamp()); } @Test @@ -70,7 +72,7 @@ void findsModsWithMetaCppInDirectSubdirectories() throws Exception { } @Test - void taskUpdatesWorkshopModThroughSteamCmdAndCachesMetadata() throws Exception { + void taskDoesNotDownloadWorkshopModWhenSteamMetadataIsCurrent() throws Exception { File oldWorkingDir = GD.WORKING_DIR; File oldDownloadsDir = GD.DOWNLOADS_DIR; String oldUserDir = System.getProperty("user.dir"); @@ -78,31 +80,55 @@ void taskUpdatesWorkshopModThroughSteamCmdAndCachesMetadata() throws Exception { try { File modDir = tempDir.resolve("mods/@CF").toFile(); modDir.mkdirs(); - writeMeta(modDir, "publishedid = 1559212036;", "name = \"CF\";"); + writeMeta(modDir, "publishedid = 1559212036;", "name = \"CF\";", "timestamp = 100;"); + Files.write(modDir.toPath().resolve("old.txt"), Arrays.asList("old"), StandardCharsets.UTF_8); + + configureUpdater("200"); + FakeSteamCMD steamCMD = new FakeSteamCMD("200", null); + TaskModsUpdater task = createTask(steamCMD); + + task.runAtStart(); + + assertEquals(1, steamCMD.detailsCalls); + assertEquals(0, steamCMD.updateCalls); + assertFalse(Files.exists(modDir.toPath().resolve("updated.txt"))); + assertTrue(task.getWarnings().isEmpty()); + + ModsConfig modsConfig = new ModsConfig(); + modsConfig.load(); + assertEquals("1559212036", modsConfig.get("mods", "CF", "steam-workshop-id").asString()); + assertEquals("200", modsConfig.get("mods", "CF", "version").asString()); + } finally { + System.setProperty("user.dir", oldUserDir); + GD.WORKING_DIR = oldWorkingDir; + GD.DOWNLOADS_DIR = oldDownloadsDir; + } + } + + @Test + void taskDownloadsWorkshopModWhenSteamMetadataIsNewerAndCachesVersion() throws Exception { + File oldWorkingDir = GD.WORKING_DIR; + File oldDownloadsDir = GD.DOWNLOADS_DIR; + String oldUserDir = System.getProperty("user.dir"); + useWorkingDir(tempDir); + try { + File modDir = tempDir.resolve("mods/@CF").toFile(); + modDir.mkdirs(); + writeMeta(modDir, "publishedid = 1559212036;", "name = \"CF\";", "timestamp = 100;"); Files.write(modDir.toPath().resolve("old.txt"), Arrays.asList("old"), StandardCharsets.UTF_8); File downloadedDir = tempDir.resolve("steamcmd/steamapps/workshop/content/221100/1559212036").toFile(); downloadedDir.mkdirs(); + writeMeta(downloadedDir, "publishedid = 1559212036;", "name = \"CF\";", "timestamp = 200;"); Files.write(downloadedDir.toPath().resolve("updated.txt"), Arrays.asList("updated"), StandardCharsets.UTF_8); - UpdaterConfig updaterConfig = new UpdaterConfig(); - updaterConfig.mods_updater.setValues("true"); - updaterConfig.mods_updater_profile.setValues("AUTOMATIC"); - updaterConfig.mods_updater_path.setValues("./mods"); - updaterConfig.mods_updater_async.setValues("false"); - updaterConfig.server_software.setValues("221100"); - updaterConfig.save(); - - FakeSteamCMD steamCMD = new FakeSteamCMD(downloadedDir); - TaskModsUpdater task = new TaskModsUpdater("ModsUpdater", new BThreadManager()) { - @Override - SteamCMD createSteamCMD() { - return steamCMD; - } - }; + configureUpdater("100"); + FakeSteamCMD steamCMD = new FakeSteamCMD("200", downloadedDir); + TaskModsUpdater task = createTask(steamCMD); task.runAtStart(); + assertEquals(1, steamCMD.detailsCalls); assertEquals(1, steamCMD.updateCalls); assertEquals("221100", steamCMD.workshopAppId); assertEquals("1559212036", steamCMD.workshopItemId); @@ -112,6 +138,7 @@ SteamCMD createSteamCMD() { ModsConfig modsConfig = new ModsConfig(); modsConfig.load(); assertEquals("1559212036", modsConfig.get("mods", "CF", "steam-workshop-id").asString()); + assertEquals("200", modsConfig.get("mods", "CF", "version").asString()); } finally { System.setProperty("user.dir", oldUserDir); GD.WORKING_DIR = oldWorkingDir; @@ -136,16 +163,50 @@ private void useWorkingDir(Path dir) throws IOException { new AL().start("AL", true, logFile, false, false); } + private void configureUpdater(String cachedWorkshopVersion) throws Exception { + UpdaterConfig updaterConfig = new UpdaterConfig(); + updaterConfig.mods_updater.setValues("true"); + updaterConfig.mods_updater_profile.setValues("AUTOMATIC"); + updaterConfig.mods_updater_path.setValues("./mods"); + updaterConfig.mods_updater_version.setValues("1.20.1"); + updaterConfig.mods_updater_async.setValues("false"); + updaterConfig.server_software.setValues("221100"); + updaterConfig.save(); + + ModsConfig modsConfig = new ModsConfig(); + modsConfig.put("mods", "CF", "version").setValues(cachedWorkshopVersion); + modsConfig.put("mods", "CF", "steam-workshop-id").setValues("1559212036"); + modsConfig.save(); + } + + private TaskModsUpdater createTask(FakeSteamCMD steamCMD) { + return new TaskModsUpdater("ModsUpdater", new BThreadManager()) { + @Override + SteamCMD createSteamCMD() { + return steamCMD; + } + }; + } + private static class FakeSteamCMD extends SteamCMD { final File workshopItemDir; + final String latestVersion; + int detailsCalls; int updateCalls; String workshopAppId; String workshopItemId; - FakeSteamCMD(File workshopItemDir) { + FakeSteamCMD(String latestVersion, File workshopItemDir) { + this.latestVersion = latestVersion; this.workshopItemDir = workshopItemDir; } + @Override + public SteamWorkshopItemDetails getWorkshopItemDetails(String workshopItemId) { + detailsCalls++; + return new SteamWorkshopItemDetails(workshopItemId, "CF", latestVersion, null); + } + @Override public boolean installOrUpdateWorkshopItem(String workshopAppId, String workshopItemId, Consumer onLog, Consumer onLogErr) { this.updateCalls++;