From 759daa1bc3ee0f17f8c1be7e42f9d505538b8d66 Mon Sep 17 00:00:00 2001 From: LostLuma Date: Wed, 24 Apr 2024 20:36:39 +0200 Subject: [PATCH 1/3] Mention new loader version in loader update info --- .../mod/fabric/FabricLoaderUpdateChecker.java | 13 ++++++- .../mod/quilt/QuiltLoaderUpdateChecker.java | 38 ++++++++++--------- .../resources/assets/modmenu/lang/en_us.json | 1 + 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricLoaderUpdateChecker.java b/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricLoaderUpdateChecker.java index fcaeb16e..513714bd 100644 --- a/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricLoaderUpdateChecker.java +++ b/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricLoaderUpdateChecker.java @@ -5,6 +5,7 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -20,6 +21,7 @@ import net.fabricmc.loader.api.SemanticVersion; import net.fabricmc.loader.api.Version; import net.fabricmc.loader.api.VersionParsingException; +import net.minecraft.text.Text; public class FabricLoaderUpdateChecker implements UpdateChecker { public static final Logger LOGGER = LoggerFactory.getLogger("Mod Menu/Fabric Update Checker"); @@ -111,7 +113,7 @@ private static UpdateInfo checkForUpdates0() throws IOException, InterruptedExce } LOGGER.debug("Fabric Loader has a matching update available!"); - return new FabricLoaderUpdateInfo(stableVersion); + return new FabricLoaderUpdateInfo(match.getFriendlyString(), stableVersion); } private static boolean isNewer(Version self, Version other) { @@ -123,9 +125,11 @@ private static Version getCurrentVersion() { } private static class FabricLoaderUpdateInfo implements UpdateInfo { + private final String version; private final boolean isStable; - private FabricLoaderUpdateInfo(boolean isStable) { + private FabricLoaderUpdateInfo(String version, boolean isStable) { + this.version = version; this.isStable = isStable; } @@ -134,6 +138,11 @@ public boolean isUpdateAvailable() { return true; } + @Override + public @Nullable Text getUpdateMessage() { + return Text.translatable("modmenu.install_version", this.version); + } + @Override public String getDownloadLink() { return "https://fabricmc.net/use/installer"; diff --git a/src/main/java/com/terraformersmc/modmenu/util/mod/quilt/QuiltLoaderUpdateChecker.java b/src/main/java/com/terraformersmc/modmenu/util/mod/quilt/QuiltLoaderUpdateChecker.java index 9b5da457..4f7eefc2 100644 --- a/src/main/java/com/terraformersmc/modmenu/util/mod/quilt/QuiltLoaderUpdateChecker.java +++ b/src/main/java/com/terraformersmc/modmenu/util/mod/quilt/QuiltLoaderUpdateChecker.java @@ -5,6 +5,7 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import org.jetbrains.annotations.Nullable; import org.quiltmc.loader.api.QuiltLoader; import org.quiltmc.loader.api.Version; import org.quiltmc.loader.api.VersionFormatException; @@ -18,6 +19,8 @@ import com.terraformersmc.modmenu.util.HttpUtil; import com.terraformersmc.modmenu.util.JsonUtil; +import net.minecraft.text.Text; + public class QuiltLoaderUpdateChecker implements UpdateChecker { public static final Logger LOGGER = LoggerFactory.getLogger("Mod Menu/Quilt Update Checker"); private static final URI LOADER_VERSIONS = URI.create("https://meta.quiltmc.org/v3/versions/loader"); @@ -105,19 +108,7 @@ private static UpdateInfo checkForUpdates0() throws IOException, InterruptedExce } LOGGER.debug("Quilt Loader has a matching update available!"); - - UpdateChannel updateChannel; - var preRelease = match.preRelease(); - - if (preRelease.isEmpty()) { - updateChannel = UpdateChannel.RELEASE; - } else if (isStableOrBeta(preRelease)) { - updateChannel = UpdateChannel.BETA; - } else { - updateChannel = UpdateChannel.ALPHA; - } - - return new QuiltLoaderUpdateInfo(updateChannel); + return new QuiltLoaderUpdateInfo(match); } private static boolean isNewer(Version.Semantic self, Version.Semantic other) { @@ -133,10 +124,10 @@ private static boolean isStableOrBeta(String preRelease) { } private static class QuiltLoaderUpdateInfo implements UpdateInfo { - private final UpdateChannel updateChannel; + private final Version.Semantic version; - private QuiltLoaderUpdateInfo(UpdateChannel updateChannel) { - this.updateChannel = updateChannel; + private QuiltLoaderUpdateInfo(Version.Semantic version) { + this.version = version; } @Override @@ -144,6 +135,11 @@ public boolean isUpdateAvailable() { return true; } + @Override + public @Nullable Text getUpdateMessage() { + return Text.translatable("modmenu.install_version", this.version.raw()); + } + @Override public String getDownloadLink() { return "https://quiltmc.org/en/install/client"; @@ -151,7 +147,15 @@ public String getDownloadLink() { @Override public UpdateChannel getUpdateChannel() { - return this.updateChannel; + var preRelease = this.version.preRelease(); + + if (preRelease.isEmpty()) { + return UpdateChannel.RELEASE; + } else if (isStableOrBeta(preRelease)) { + return UpdateChannel.BETA; + } else { + return UpdateChannel.ALPHA; + } } } } diff --git a/src/main/resources/assets/modmenu/lang/en_us.json b/src/main/resources/assets/modmenu/lang/en_us.json index 7e4544ff..ba9380a9 100644 --- a/src/main/resources/assets/modmenu/lang/en_us.json +++ b/src/main/resources/assets/modmenu/lang/en_us.json @@ -63,6 +63,7 @@ "modmenu.experimental": "(Mod Menu update checker is experimental!)", "modmenu.childHasUpdate": "A child of this mod has an update available.", "modmenu.updateText": "v%s on %s", + "modmenu.install_version": "Install version %s", "modmenu.downloadLink": "Download", "modmenu.buymeacoffee": "Buy Me a Coffee", From 89425258881d6ac6bc6c4e0863e5d9c03fbe3f82 Mon Sep 17 00:00:00 2001 From: LostLuma Date: Wed, 24 Apr 2024 20:38:05 +0200 Subject: [PATCH 2/3] Remove special casing during rendering of Modrinth update info --- .../gui/widget/DescriptionListWidget.java | 35 +++++++------------ .../modmenu/util/mod/ModrinthUpdateInfo.java | 12 +++++++ 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/terraformersmc/modmenu/gui/widget/DescriptionListWidget.java b/src/main/java/com/terraformersmc/modmenu/gui/widget/DescriptionListWidget.java index c7b8b666..0155f66b 100644 --- a/src/main/java/com/terraformersmc/modmenu/gui/widget/DescriptionListWidget.java +++ b/src/main/java/com/terraformersmc/modmenu/gui/widget/DescriptionListWidget.java @@ -37,7 +37,6 @@ public class DescriptionListWidget extends EntryListWidget Date: Wed, 24 Apr 2024 20:42:10 +0200 Subject: [PATCH 3/3] Fix built-in update checker promoting older updates This happens if the user is on a beta but only requests release update info. --- .../modmenu/util/UpdateCheckerThread.java | 17 -- .../util/UpdateCheckerThreadFactory.java | 16 ++ .../modmenu/util/UpdateCheckerUtil.java | 228 ++++++++++++++---- 3 files changed, 197 insertions(+), 64 deletions(-) delete mode 100644 src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerThread.java create mode 100644 src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerThreadFactory.java diff --git a/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerThread.java b/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerThread.java deleted file mode 100644 index 67b02b72..00000000 --- a/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerThread.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.terraformersmc.modmenu.util; - -import com.terraformersmc.modmenu.util.mod.Mod; - -public class UpdateCheckerThread extends Thread { - - protected UpdateCheckerThread(Mod mod, Runnable runnable) { - super(runnable); - setDaemon(true); - setName("Update Checker/%s".formatted(mod.getName())); - } - - public static void run(Mod mod, Runnable runnable) { - new UpdateCheckerThread(mod, runnable).start(); - } - -} diff --git a/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerThreadFactory.java b/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerThreadFactory.java new file mode 100644 index 00000000..92466ab3 --- /dev/null +++ b/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerThreadFactory.java @@ -0,0 +1,16 @@ +package com.terraformersmc.modmenu.util; + +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +public class UpdateCheckerThreadFactory implements ThreadFactory { + static final AtomicInteger COUNT = new AtomicInteger(-1); + + @Override + public Thread newThread(@NotNull Runnable r) { + var index = COUNT.incrementAndGet(); + return Thread.ofVirtual().name("ModMenu/Update Checker/%s".formatted(index)).unstarted(r); + } +} diff --git a/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerUtil.java b/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerUtil.java index a9423ae2..15cd08c6 100644 --- a/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerUtil.java +++ b/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerUtil.java @@ -6,16 +6,17 @@ import com.terraformersmc.modmenu.ModMenu; import com.terraformersmc.modmenu.api.UpdateChannel; import com.terraformersmc.modmenu.api.UpdateChecker; +import com.terraformersmc.modmenu.api.UpdateInfo; import com.terraformersmc.modmenu.config.ModMenuConfig; import com.terraformersmc.modmenu.util.mod.Mod; import com.terraformersmc.modmenu.util.mod.ModrinthUpdateInfo; -import net.fabricmc.loader.api.FabricLoader; import net.minecraft.SharedConstants; import net.minecraft.client.MinecraftClient; import net.minecraft.client.toast.SystemToast; import net.minecraft.text.Text; -import net.minecraft.util.Util; +import net.minecraft.util.Util; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,7 +24,11 @@ import java.net.URI; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.time.Instant; +import java.time.format.DateTimeParseException; import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; public class UpdateCheckerUtil { public static final Logger LOGGER = LoggerFactory.getLogger("Mod Menu/Update Checker"); @@ -40,38 +45,91 @@ public static void checkForUpdates() { } LOGGER.info("Checking mod updates..."); - Util.getMainWorkerExecutor().execute(UpdateCheckerUtil::checkForModrinthUpdates); - checkForCustomUpdates(); + Util.getMainWorkerExecutor().execute(UpdateCheckerUtil::checkForUpdates0); } - public static void checkForCustomUpdates() { - ModMenu.MODS.values().stream().filter(UpdateCheckerUtil::allowsUpdateChecks).forEach(mod -> { - UpdateChecker updateChecker = mod.getUpdateChecker(); + private static void checkForUpdates0() { + try (var executor = Executors.newThreadPerTaskExecutor(new UpdateCheckerThreadFactory())) { + List withoutUpdateChecker = new ArrayList<>(); + + ModMenu.MODS.values().stream().filter(UpdateCheckerUtil::allowsUpdateChecks).forEach(mod -> { + UpdateChecker updateChecker = mod.getUpdateChecker(); + + if (updateChecker == null) { + withoutUpdateChecker.add(mod); // Fall back to update checking via Modrinth + } else { + executor.submit(() -> { + // We don't know which mod the thread is for yet in the thread factory + Thread.currentThread().setName("ModMenu/Update Checker/%s".formatted(mod.getName())); + + var update = updateChecker.checkForUpdates(); + + if (update == null) { + return; + } + + mod.setUpdateInfo(update); + LOGGER.info("Update available for '{}@{}'", mod.getId(), mod.getVersion()); + }); + } + }); + + if (modrinthApiV2Deprecated) { + return; + } + + var modHashes = getModHashes(withoutUpdateChecker); + + var currentVersionsFuture = executor.submit(() -> getCurrentVersions(modHashes.keySet())); + var updatedVersionsFuture = executor.submit(() -> getUpdatedVersions(modHashes.keySet())); + + Map currentVersions = null; + Map updatedVersions = null; + + try { + currentVersions = currentVersionsFuture.get(); + updatedVersions = updatedVersionsFuture.get(); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } - if (updateChecker == null) { + if (currentVersions == null || updatedVersions == null) { return; } - UpdateCheckerThread.run(mod, () -> { - var update = updateChecker.checkForUpdates(); + for (var hash : modHashes.keySet()) { + var date = currentVersions.get(hash); + var data = updatedVersions.get(hash); - if (update == null) { - return; + if (date == null || data == null) { + continue; } - mod.setUpdateInfo(update); - LOGGER.info("Update available for '{}@{}'", mod.getId(), mod.getVersion()); - }); - }); - } + // Current version is still the newest + if (Objects.equals(hash, data.hash)) { + continue; + } - public static void checkForModrinthUpdates() { - if (modrinthApiV2Deprecated) { - return; + // Current version is newer than what's + // Available on our preferred update channel + if (date.compareTo(data.releaseDate) >= 0) { + continue; + } + + for (var mod : modHashes.get(hash)) { + mod.setUpdateInfo(data.asUpdateInfo()); + LOGGER.info("Update available for '{}@{}', (-> {})", mod.getId(), mod.getVersion(), data.versionNumber); + } + } } + } - Map> modHashes = new HashMap<>(); - new ArrayList<>(ModMenu.MODS.values()).stream().filter(UpdateCheckerUtil::allowsUpdateChecks).filter(mod -> mod.getUpdateChecker() == null).forEach(mod -> { + private static Map> getModHashes(Collection mods) { + Map> results = new HashMap<>(); + + for (var mod : mods) { String modId = mod.getId(); try { @@ -79,14 +137,88 @@ public static void checkForModrinthUpdates() { if (hash != null) { LOGGER.debug("Hash for {} is {}", modId, hash); - modHashes.putIfAbsent(hash, new HashSet<>()); - modHashes.get(hash).add(mod); + results.putIfAbsent(hash, new HashSet<>()); + results.get(hash).add(mod); } } catch (IOException e) { LOGGER.error("Error getting mod hash for mod {}: ", modId, e); } - }); + }; + + return results; + } + + public static void triggerV2DeprecatedToast() { + if (modrinthApiV2Deprecated && ModMenuConfig.UPDATE_CHECKER.getValue()) { + MinecraftClient.getInstance().getToastManager().add(new SystemToast( + SystemToast.Type.PERIODIC_NOTIFICATION, + Text.translatable("modmenu.modrinth.v2_deprecated.title"), + Text.translatable("modmenu.modrinth.v2_deprecated.description") + )); + } + } + + /** + * @return a map of file hash to its release date on Modrinth. + */ + private static @Nullable Map getCurrentVersions(Collection modHashes) { + String body = ModMenu.GSON_MINIFIED.toJson(new CurrentVersionsFromHashes(modHashes)); + + var request = HttpRequest.newBuilder() + .POST(HttpRequest.BodyPublishers.ofString(body)) + .header("Content-Type", "application/json") + .uri(URI.create("https://api.modrinth.com/v2/version_files")); + + try { + var response = HttpUtil.request(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() == 410) { + modrinthApiV2Deprecated = true; + LOGGER.warn("Modrinth API v2 is deprecated, unable to check for mod updates."); + } else if (response.statusCode() == 200) { + Map results = new HashMap<>(); + JsonObject data = JsonParser.parseString(response.body()).getAsJsonObject(); + + data.asMap().forEach((hash, inner) -> { + Instant date; + var version = inner.getAsJsonObject(); + + try { + date = Instant.parse(version.get("date_published").getAsString()); + } catch (DateTimeParseException e) { + return; + } + + results.put(hash, date); + }); + + return results; + } + } catch (IOException | InterruptedException e) { + LOGGER.error("Error checking for versions: ", e); + } + + return null; + } + + public static class CurrentVersionsFromHashes { + public Collection hashes; + public String algorithm = "sha512"; + + public CurrentVersionsFromHashes(Collection hashes) { + this.hashes = hashes; + } + } + + private static UpdateChannel getUpdateChannel(String versionType) { + try { + return UpdateChannel.valueOf(versionType.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException | NullPointerException e) { + return UpdateChannel.RELEASE; + } + } + private static @Nullable Map getUpdatedVersions(Collection modHashes) { String mcVer = SharedConstants.getGameVersion().getName(); List loaders = ModMenu.runningQuilt ? List.of("fabric", "quilt") : List.of("fabric"); @@ -101,7 +233,7 @@ public static void checkForModrinthUpdates() { updateChannels = List.of(UpdateChannel.ALPHA, UpdateChannel.BETA, UpdateChannel.RELEASE); } - String body = ModMenu.GSON_MINIFIED.toJson(new LatestVersionsFromHashesBody(modHashes.keySet(), loaders, mcVer, updateChannels)); + String body = ModMenu.GSON_MINIFIED.toJson(new LatestVersionsFromHashesBody(modHashes, loaders, mcVer, updateChannels)); LOGGER.debug("Body: {}", body); var latestVersionsRequest = HttpRequest.newBuilder() @@ -118,6 +250,7 @@ public static void checkForModrinthUpdates() { modrinthApiV2Deprecated = true; LOGGER.warn("Modrinth API v2 is deprecated, unable to check for mod updates."); } else if (status == 200) { + Map results = new HashMap<>(); JsonObject responseObject = JsonParser.parseString(latestVersionsResponse.body()).getAsJsonObject(); LOGGER.debug(String.valueOf(responseObject)); responseObject.asMap().forEach((lookupHash, versionJson) -> { @@ -133,38 +266,39 @@ public static void checkForModrinthUpdates() { return; } + Instant date; + + try { + date = Instant.parse(versionObj.get("date_published").getAsString()); + } catch (DateTimeParseException e) { + return; + } + var updateChannel = UpdateCheckerUtil.getUpdateChannel(versionType); var versionHash = primaryFile.get().getAsJsonObject().get("hashes").getAsJsonObject().get("sha512").getAsString(); - if (!Objects.equals(versionHash, lookupHash)) { - // hashes different, there's an update. - modHashes.get(lookupHash).forEach(mod -> { - LOGGER.info("Update available for '{}@{}', (-> {})", mod.getId(), mod.getVersion(), versionNumber); - mod.setUpdateInfo(new ModrinthUpdateInfo(projectId, versionId, versionNumber, updateChannel)); - }); - } + results.put(lookupHash, new VersionUpdate(projectId, versionId, versionNumber, date, updateChannel, versionHash)); }); + + return results; } } catch (IOException | InterruptedException e) { LOGGER.error("Error checking for updates: ", e); } - } - private static UpdateChannel getUpdateChannel(String versionType) { - try { - return UpdateChannel.valueOf(versionType.toUpperCase(Locale.ROOT)); - } catch (IllegalArgumentException | NullPointerException e) { - return UpdateChannel.RELEASE; - } + return null; } - public static void triggerV2DeprecatedToast() { - if (modrinthApiV2Deprecated && ModMenuConfig.UPDATE_CHECKER.getValue()) { - MinecraftClient.getInstance().getToastManager().add(new SystemToast( - SystemToast.Type.PERIODIC_NOTIFICATION, - Text.translatable("modmenu.modrinth.v2_deprecated.title"), - Text.translatable("modmenu.modrinth.v2_deprecated.description") - )); + private record VersionUpdate( + String projectId, + String versionId, + String versionNumber, + Instant releaseDate, + UpdateChannel updateChannel, + String hash + ) { + private UpdateInfo asUpdateInfo() { + return new ModrinthUpdateInfo(this.projectId, this.versionId, this.versionNumber, this.updateChannel); } }