From 7e29f74f2de8c61616abbed292791f26363c6c80 Mon Sep 17 00:00:00 2001 From: Alex Gazmanovich <128359229+Gazmanovich@users.noreply.github.com> Date: Wed, 6 Mar 2024 22:27:16 +0300 Subject: [PATCH 01/10] Update be_by.json (#696) --- src/main/resources/assets/modmenu/lang/be_by.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/resources/assets/modmenu/lang/be_by.json b/src/main/resources/assets/modmenu/lang/be_by.json index 7c7d33e6..de5aab63 100644 --- a/src/main/resources/assets/modmenu/lang/be_by.json +++ b/src/main/resources/assets/modmenu/lang/be_by.json @@ -50,7 +50,7 @@ "modmenu.viewCredits": "Паглядзець аўтараў", "modmenu.license": "Ліцэнзія:", "modmenu.links": "Спасылкі:", - "modmenu.source": "Крыінца", + "modmenu.source": "Крыніца", "modmenu.hasUpdate": "Даступна абнаўленне:", "modmenu.childHasUpdate": "Абнаўленне дзіцячага мода даступна.", "modmenu.updateText": "v%s на %s", @@ -72,7 +72,7 @@ "modmenu.paypal": "PayPal", "modmenu.reddit": "Reddit", "modmenu.twitch": "Twitch", - "modmenu.twitter": "Twitter", + "modmenu.twitter": "X (былы Twitter)", "modmenu.wiki": "Wiki", "modmenu.youtube": "YouTube", "modmenu.modsFolder": "Адкрыць папку модаў", @@ -121,7 +121,7 @@ "option.modmenu.mod_count_location.mods_button": "Кнопка модаў", "option.modmenu.mod_count_location.title_screen_and_mods_button": "Абодва", "option.modmenu.mod_count_location.none": "Ніяк", - "option.modmenu.easter_eggs": "Схованкі", + "option.modmenu.easter_eggs": "Сакрэты", "option.modmenu.easter_eggs.true": "Уключаны", "option.modmenu.easter_eggs.false": "Адключаны", "option.modmenu.mods_button_style": "Кнопка модаў", From 57c5ca8a083383c645cb69ffdf227e7cdd23bc5c Mon Sep 17 00:00:00 2001 From: Prospector Date: Fri, 8 Mar 2024 12:36:31 -0800 Subject: [PATCH 02/10] Re-enable CurseForge publishing, bump versions --- .github/workflows/release.yml | 2 +- build.gradle | 2 +- gradle.properties | 10 +++++++++- src/main/resources/fabric.mod.json | 5 +---- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c63ebe18..d00d34a5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,7 +45,7 @@ jobs: if: ${{ runner.os != 'Windows' }} run: chmod +x ./gradlew - name: Build - run: ./gradlew generateChangelog build publish github modrinth --stacktrace --parallel -PlastTag="v${{ github.event.inputs.previousVersion }}" -PcurrentTag="v${{ github.event.inputs.version }}" + run: ./gradlew generateChangelog build publish github modrinth curseforge --stacktrace --parallel -PlastTag="v${{ github.event.inputs.previousVersion }}" -PcurrentTag="v${{ github.event.inputs.version }}" env: MAVEN_URL: ${{ secrets.MAVEN_URL }} MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} diff --git a/build.gradle b/build.gradle index 4fd229b6..e2a6bd5b 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'fabric-loom' version '1.1-SNAPSHOT' } -apply from: 'https://raw.githubusercontent.com/TerraformersMC/GradleScripts/2.5+no-cf/ferry.gradle' +apply from: 'https://raw.githubusercontent.com/TerraformersMC/GradleScripts/2.5/ferry.gradle' dependencies { minecraft "com.mojang:minecraft:$project.minecraft_version" diff --git a/gradle.properties b/gradle.properties index 19520f96..cde5b04f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,9 +20,16 @@ default_release_type=stable # Modrinth Metadata modrinth_slug=modmenu modrinth_id=mOgUt4GM -modrinth_game_versions=1.20.3-pre4, 1.20.3-rc1, 1.20.3, 1.20.4-rc1, 1.20.4, 23w51a, 23w51b +modrinth_game_versions=1.20.4 modrinth_mod_loaders=fabric, quilt +# CurseForge Metadata +curseforge_slug=modmenu +curseforge_id=308702 +curseforge_game_versions=1.20.4, Fabric, Quilt +curseforge_required_dependencies= +curseforge_optional_dependencies= + # Mod Loader Metadata loader_icon=https://raw.githubusercontent.com/Prospector/images/master/fabric-quilt-tiny.png loader_name=Fabric/Quilt @@ -30,6 +37,7 @@ loader_name=Fabric/Quilt # Discord Emotes modrinth_emote=<:modrinth:802414390510354453> github_emote=<:github:698031289223217152> +curseforge_emote=<:curseforge:1078567270239981628> # Webhook Options use_project_username=false diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 493f8693..7cc3bb6a 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -25,10 +25,7 @@ "fabric-key-binding-api-v1": "*", "fabric-lifecycle-events-v1": "*", "fabricloader": ">=0.12.23", - "minecraft": ">=1.20.3-" - }, - "breaks" : { - "better_mod_button": "*" + "minecraft": "1.20.4" }, "authors": [ "Prospector", From b19bbacd56f8a037ad401cf91192bca450eabdb7 Mon Sep 17 00:00:00 2001 From: Prospector Date: Fri, 8 Mar 2024 12:09:08 -0800 Subject: [PATCH 03/10] Update twitter name - Change Twitter name to X (Twitter) --- src/main/resources/assets/modmenu/lang/en_us.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/assets/modmenu/lang/en_us.json b/src/main/resources/assets/modmenu/lang/en_us.json index 9ad3c95e..5d02428b 100644 --- a/src/main/resources/assets/modmenu/lang/en_us.json +++ b/src/main/resources/assets/modmenu/lang/en_us.json @@ -82,7 +82,7 @@ "modmenu.paypal": "PayPal", "modmenu.reddit": "Reddit", "modmenu.twitch": "Twitch", - "modmenu.twitter": "Twitter", + "modmenu.twitter": "X (Twitter)", "modmenu.wiki": "Wiki", "modmenu.youtube": "YouTube", From 500618783a80ee6bb92f4cc8321c8fc3ec2c579f Mon Sep 17 00:00:00 2001 From: Prospector Date: Fri, 8 Mar 2024 12:42:18 -0800 Subject: [PATCH 04/10] Delay handling API implementations until it's first needed - Delay loading API implementations until it's first needed in attempt to fix Architectury API's usage (beta) --- .../com/terraformersmc/modmenu/ModMenu.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/terraformersmc/modmenu/ModMenu.java b/src/main/java/com/terraformersmc/modmenu/ModMenu.java index 131438e0..5ce9abd2 100644 --- a/src/main/java/com/terraformersmc/modmenu/ModMenu.java +++ b/src/main/java/com/terraformersmc/modmenu/ModMenu.java @@ -1,6 +1,5 @@ package com.terraformersmc.modmenu; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.LinkedListMultimap; import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; @@ -29,7 +28,6 @@ import java.text.NumberFormat; import java.util.*; -import java.util.function.Supplier; public class ModMenu implements ClientModInitializer { public static final String MOD_ID = "modmenu"; @@ -42,17 +40,19 @@ public class ModMenu implements ClientModInitializer { public static final Map ROOT_MODS = new HashMap<>(); public static final LinkedListMultimap PARENT_MAP = LinkedListMultimap.create(); - private static Map> configScreenFactories = new HashMap<>(); - private static List>> delayedScreenFactoryProviders = new ArrayList<>(); + private static final Map> configScreenFactories = new HashMap<>(); + private static final List apiImplementations = new ArrayList<>(); private static int cachedDisplayedModCount = -1; public static boolean runningQuilt = FabricLoader.getInstance().isModLoaded("quilt_loader"); public static boolean devEnvironment = FabricLoader.getInstance().isDevelopmentEnvironment(); public static Screen getConfigScreen(String modid, Screen menuScreen) { - if(!delayedScreenFactoryProviders.isEmpty()) { - delayedScreenFactoryProviders.forEach(map -> map.forEach(configScreenFactories::putIfAbsent)); - delayedScreenFactoryProviders.clear(); + for (ModMenuApi api : apiImplementations) { + var factoryProviders = api.getProvidedConfigScreenFactories(); + if (!factoryProviders.isEmpty()) { + factoryProviders.forEach(configScreenFactories::putIfAbsent); + } } if (ModMenuConfig.HIDDEN_CONFIGS.getValue().contains(modid)) { return null; @@ -74,7 +74,7 @@ public void onInitializeClient() { try { ModMenuApi api = entrypoint.getEntrypoint(); configScreenFactories.put(modId, api.getModConfigScreenFactory()); - delayedScreenFactoryProviders.add(api.getProvidedConfigScreenFactories()); + apiImplementations.add(api); api.attachModpackBadges(modpackMods::add); } catch (Throwable e) { LOGGER.error("Mod {} provides a broken implementation of ModMenuApi", modId, e); @@ -148,9 +148,9 @@ public static String getDisplayedModCount() { if (cachedDisplayedModCount == -1) { // listen, if you have >= 2^32 mods then that's on you cachedDisplayedModCount = Math.toIntExact(MODS.values().stream().filter(mod -> - (ModMenuConfig.COUNT_CHILDREN.getValue() || mod.getParent() == null) && - (ModMenuConfig.COUNT_LIBRARIES.getValue() || !mod.getBadges().contains(Mod.Badge.LIBRARY)) && - (ModMenuConfig.COUNT_HIDDEN_MODS.getValue() || !mod.isHidden()) + (ModMenuConfig.COUNT_CHILDREN.getValue() || mod.getParent() == null) && + (ModMenuConfig.COUNT_LIBRARIES.getValue() || !mod.getBadges().contains(Mod.Badge.LIBRARY)) && + (ModMenuConfig.COUNT_HIDDEN_MODS.getValue() || !mod.isHidden()) ).count()); } return NumberFormat.getInstance().format(cachedDisplayedModCount); From fd9df7b6988d86ae5bd1264eea04b6c718eb6e23 Mon Sep 17 00:00:00 2001 From: Prospector <6166773+Prospector@users.noreply.github.com> Date: Thu, 18 Apr 2024 08:55:10 -0700 Subject: [PATCH 05/10] Add support for custom update checking logic (#708) * Add support for custom update checking logic * Rename ModrinthUtil to UpdateCheckerUtil * Make download link required * Fix search * Use a separate thread for every custom update checker --------- Co-authored-by: Max Henkel --- .../com/terraformersmc/modmenu/ModMenu.java | 11 +- .../modmenu/api/ModMenuApi.java | 10 ++ .../modmenu/api/UpdateChecker.java | 13 ++ .../modmenu/api/UpdateInfo.java | 26 +++ .../modmenu/event/ModMenuEventHandler.java | 4 +- .../gui/widget/DescriptionListWidget.java | 37 +++- .../gui/widget/entries/ModListEntry.java | 2 +- .../modmenu/util/ModrinthUtil.java | 143 ---------------- .../modmenu/util/UpdateCheckerThread.java | 17 ++ .../modmenu/util/UpdateCheckerUtil.java | 160 ++++++++++++++++++ .../terraformersmc/modmenu/util/mod/Mod.java | 27 ++- .../modmenu/util/mod/ModSearch.java | 3 +- .../modmenu/util/mod/ModrinthData.java | 8 - .../modmenu/util/mod/ModrinthUpdateInfo.java | 39 +++++ .../util/mod/fabric/FabricDummyParentMod.java | 23 ++- .../modmenu/util/mod/fabric/FabricMod.java | 30 +++- .../modmenu/util/mod/quilt/QuiltMod.java | 7 +- .../resources/assets/modmenu/lang/en_us.json | 1 + 18 files changed, 367 insertions(+), 194 deletions(-) create mode 100644 src/main/java/com/terraformersmc/modmenu/api/UpdateChecker.java create mode 100644 src/main/java/com/terraformersmc/modmenu/api/UpdateInfo.java delete mode 100644 src/main/java/com/terraformersmc/modmenu/util/ModrinthUtil.java create mode 100644 src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerThread.java create mode 100644 src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerUtil.java delete mode 100644 src/main/java/com/terraformersmc/modmenu/util/mod/ModrinthData.java create mode 100644 src/main/java/com/terraformersmc/modmenu/util/mod/ModrinthUpdateInfo.java diff --git a/src/main/java/com/terraformersmc/modmenu/ModMenu.java b/src/main/java/com/terraformersmc/modmenu/ModMenu.java index 5ce9abd2..07ddcd3a 100644 --- a/src/main/java/com/terraformersmc/modmenu/ModMenu.java +++ b/src/main/java/com/terraformersmc/modmenu/ModMenu.java @@ -6,11 +6,12 @@ import com.google.gson.GsonBuilder; import com.terraformersmc.modmenu.api.ConfigScreenFactory; import com.terraformersmc.modmenu.api.ModMenuApi; +import com.terraformersmc.modmenu.api.UpdateChecker; import com.terraformersmc.modmenu.config.ModMenuConfig; import com.terraformersmc.modmenu.config.ModMenuConfigManager; import com.terraformersmc.modmenu.event.ModMenuEventHandler; import com.terraformersmc.modmenu.util.ModMenuScreenTexts; -import com.terraformersmc.modmenu.util.ModrinthUtil; +import com.terraformersmc.modmenu.util.UpdateCheckerUtil; import com.terraformersmc.modmenu.util.mod.Mod; import com.terraformersmc.modmenu.util.mod.fabric.FabricDummyParentMod; import com.terraformersmc.modmenu.util.mod.fabric.FabricMod; @@ -68,6 +69,7 @@ public static Screen getConfigScreen(String modid, Screen menuScreen) { public void onInitializeClient() { ModMenuConfigManager.initializeConfig(); Set modpackMods = new HashSet<>(); + Map updateCheckers = new HashMap<>(); FabricLoader.getInstance().getEntrypointContainers("modmenu", ModMenuApi.class).forEach(entrypoint -> { ModMetadata metadata = entrypoint.getProvider().getMetadata(); String modId = metadata.getId(); @@ -75,6 +77,7 @@ public void onInitializeClient() { ModMenuApi api = entrypoint.getEntrypoint(); configScreenFactories.put(modId, api.getModConfigScreenFactory()); apiImplementations.add(api); + updateCheckers.put(modId, api.getUpdateChecker()); api.attachModpackBadges(modpackMods::add); } catch (Throwable e) { LOGGER.error("Mod {} provides a broken implementation of ModMenuApi", modId, e); @@ -91,10 +94,12 @@ public void onInitializeClient() { mod = new FabricMod(modContainer, modpackMods); } + mod.setUpdateChecker(updateCheckers.get(mod.getId())); + MODS.put(mod.getId(), mod); } - ModrinthUtil.checkForUpdates(); + UpdateCheckerUtil.checkForUpdates(); Map dummyParents = new HashMap<>(); @@ -136,7 +141,7 @@ public static boolean areModUpdatesAvailable() { continue; } - if (mod.getModrinthData() != null || mod.getChildHasUpdate()) { + if (mod.hasUpdate() || mod.getChildHasUpdate()) { return true; // At least one currently visible mod has an update } } diff --git a/src/main/java/com/terraformersmc/modmenu/api/ModMenuApi.java b/src/main/java/com/terraformersmc/modmenu/api/ModMenuApi.java index ef66bdc9..4a6bc27b 100644 --- a/src/main/java/com/terraformersmc/modmenu/api/ModMenuApi.java +++ b/src/main/java/com/terraformersmc/modmenu/api/ModMenuApi.java @@ -43,6 +43,16 @@ default ConfigScreenFactory getModConfigScreenFactory() { return screen -> null; } + /** + * Used for mods that have their own update checking logic. + * By returning your own {@link UpdateChecker} instance, you can override ModMenus built-in update checking logic. + * + * @return An {@link UpdateChecker} or null if ModMenu should handle update checking. + */ + default UpdateChecker getUpdateChecker() { + return null; + } + /** * Used to provide config screen factories for other mods. This takes second * priority to a mod's own config screen factory provider. For example, if diff --git a/src/main/java/com/terraformersmc/modmenu/api/UpdateChecker.java b/src/main/java/com/terraformersmc/modmenu/api/UpdateChecker.java new file mode 100644 index 00000000..1c2aa231 --- /dev/null +++ b/src/main/java/com/terraformersmc/modmenu/api/UpdateChecker.java @@ -0,0 +1,13 @@ +package com.terraformersmc.modmenu.api; + +public interface UpdateChecker { + + /** + * Gets called when ModMenu is checking for updates. + * This is done in a separate thread, so this call can/should be blocking. + * + * @return The update info + */ + UpdateInfo checkForUpdates(); + +} diff --git a/src/main/java/com/terraformersmc/modmenu/api/UpdateInfo.java b/src/main/java/com/terraformersmc/modmenu/api/UpdateInfo.java new file mode 100644 index 00000000..fee9c07b --- /dev/null +++ b/src/main/java/com/terraformersmc/modmenu/api/UpdateInfo.java @@ -0,0 +1,26 @@ +package com.terraformersmc.modmenu.api; + +import net.minecraft.text.Text; +import org.jetbrains.annotations.Nullable; + +public interface UpdateInfo { + + /** + * @return If an update for the mod is available. + */ + boolean isUpdateAvailable(); + + /** + * @return The message that is getting displayed when an update is available or null to let ModMenu handle displaying the message. + */ + @Nullable + default Text getUpdateMessage() { + return null; + } + + /** + * @return The URL to the mod download. + */ + String getDownloadLink(); + +} diff --git a/src/main/java/com/terraformersmc/modmenu/event/ModMenuEventHandler.java b/src/main/java/com/terraformersmc/modmenu/event/ModMenuEventHandler.java index 1d9291e5..e27c22d2 100644 --- a/src/main/java/com/terraformersmc/modmenu/event/ModMenuEventHandler.java +++ b/src/main/java/com/terraformersmc/modmenu/event/ModMenuEventHandler.java @@ -6,7 +6,7 @@ import com.terraformersmc.modmenu.gui.ModsScreen; import com.terraformersmc.modmenu.gui.widget.ModMenuButtonWidget; import com.terraformersmc.modmenu.gui.widget.UpdateCheckerTexturedButtonWidget; -import com.terraformersmc.modmenu.util.ModrinthUtil; +import com.terraformersmc.modmenu.util.UpdateCheckerUtil; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; @@ -91,7 +91,7 @@ private static void afterTitleScreenInit(Screen screen) { } } } - ModrinthUtil.triggerV2DeprecatedToast(); + UpdateCheckerUtil.triggerV2DeprecatedToast(); } private static void onClientEndTick(MinecraftClient client) { 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 836f6494..b600be89 100644 --- a/src/main/java/com/terraformersmc/modmenu/gui/widget/DescriptionListWidget.java +++ b/src/main/java/com/terraformersmc/modmenu/gui/widget/DescriptionListWidget.java @@ -2,11 +2,13 @@ import com.mojang.blaze3d.platform.GlStateManager; import com.mojang.blaze3d.systems.RenderSystem; +import com.terraformersmc.modmenu.api.UpdateInfo; import com.terraformersmc.modmenu.config.ModMenuConfig; import com.terraformersmc.modmenu.gui.ModsScreen; import com.terraformersmc.modmenu.gui.widget.entries.ModListEntry; import com.terraformersmc.modmenu.util.VersionUtil; import com.terraformersmc.modmenu.util.mod.Mod; +import com.terraformersmc.modmenu.util.mod.ModrinthUpdateInfo; import net.minecraft.client.MinecraftClient; import net.minecraft.client.font.TextRenderer; import net.minecraft.client.gui.DrawContext; @@ -37,6 +39,7 @@ public class DescriptionListWidget extends EntryListWidget { - LOGGER.info("Checking mod updates..."); - - Map> modHashes = new HashMap<>(); - new ArrayList<>(ModMenu.MODS.values()).stream().filter(ModrinthUtil::allowsUpdateChecks).forEach(mod -> { - String modId = mod.getId(); - - try { - String hash = mod.getSha512Hash(); - - if (hash != null) { - LOGGER.debug("Hash for {} is {}", modId, hash); - modHashes.putIfAbsent(hash, new HashSet<>()); - modHashes.get(hash).add(mod); - } - } catch (IOException e) { - LOGGER.error("Error getting mod hash for mod {}: ", modId, e); - } - }); - - String environment = ModMenu.devEnvironment ? "/development": ""; - String primaryLoader = ModMenu.runningQuilt ? "quilt" : "fabric"; - List loaders = ModMenu.runningQuilt ? List.of("fabric", "quilt") : List.of("fabric"); - - String mcVer = SharedConstants.getGameVersion().getName(); - String[] splitVersion = FabricLoader.getInstance().getModContainer(ModMenu.MOD_ID) - .get().getMetadata().getVersion().getFriendlyString().split("\\+", 1); // Strip build metadata for privacy - final var modMenuVersion = splitVersion.length > 1 ? splitVersion[1] : splitVersion[0]; - final var userAgent = "%s/%s (%s/%s%s)".formatted(ModMenu.GITHUB_REF, modMenuVersion, mcVer, primaryLoader, environment); - String body = ModMenu.GSON_MINIFIED.toJson(new LatestVersionsFromHashesBody(modHashes.keySet(), loaders, mcVer)); - LOGGER.debug("User agent: " + userAgent); - LOGGER.debug("Body: " + body); - var latestVersionsRequest = HttpRequest.newBuilder() - .POST(HttpRequest.BodyPublishers.ofString(body)) - .header("User-Agent", userAgent) - .header("Content-Type", "application/json") - .uri(URI.create("https://api.modrinth.com/v2/version_files/update")) - .build(); - - try { - var latestVersionsResponse = client.send(latestVersionsRequest, HttpResponse.BodyHandlers.ofString()); - - int status = latestVersionsResponse.statusCode(); - LOGGER.debug("Status: " + status); - if (status == 410) { - apiV2Deprecated = true; - LOGGER.warn("Modrinth API v2 is deprecated, unable to check for mod updates."); - } else if (status == 200) { - JsonObject responseObject = JsonParser.parseString(latestVersionsResponse.body()).getAsJsonObject(); - LOGGER.debug(String.valueOf(responseObject)); - responseObject.asMap().forEach((lookupHash, versionJson) -> { - var versionObj = versionJson.getAsJsonObject(); - var projectId = versionObj.get("project_id").getAsString(); - var versionNumber = versionObj.get("version_number").getAsString(); - var versionId = versionObj.get("id").getAsString(); - var primaryFile = versionObj.get("files").getAsJsonArray().asList().stream() - .filter(file -> file.getAsJsonObject().get("primary").getAsBoolean()).findFirst(); - - if (primaryFile.isEmpty()) { - return; - } - - 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.setModrinthData(new ModrinthData(projectId, versionId, versionNumber)); - }); - } - }); - } - } catch (IOException | InterruptedException e) { - LOGGER.error("Error checking for updates: ", e); - } - }); - } - - public static void triggerV2DeprecatedToast() { - if (apiV2Deprecated && 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") - )); - } - } - - public static class LatestVersionsFromHashesBody { - public Collection hashes; - public String algorithm = "sha512"; - public Collection loaders; - @SerializedName("game_versions") - public Collection gameVersions; - - public LatestVersionsFromHashesBody(Collection hashes, Collection loaders, String mcVersion) { - this.hashes = hashes; - this.loaders = loaders; - this.gameVersions = Set.of(mcVersion); - } - } -} diff --git a/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerThread.java b/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerThread.java new file mode 100644 index 00000000..67b02b72 --- /dev/null +++ b/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerThread.java @@ -0,0 +1,17 @@ +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/UpdateCheckerUtil.java b/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerUtil.java new file mode 100644 index 00000000..509c2060 --- /dev/null +++ b/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerUtil.java @@ -0,0 +1,160 @@ +package com.terraformersmc.modmenu.util; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.annotations.SerializedName; +import com.terraformersmc.modmenu.ModMenu; +import com.terraformersmc.modmenu.api.UpdateChecker; +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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.*; + +public class UpdateCheckerUtil { + public static final Logger LOGGER = LoggerFactory.getLogger("Mod Menu/Update Checker"); + + private static final HttpClient client = HttpClient.newHttpClient(); + private static boolean modrinthApiV2Deprecated = false; + + private static boolean allowsUpdateChecks(Mod mod) { + return mod.allowsUpdateChecks(); + } + + public static void checkForUpdates() { + if (!ModMenuConfig.UPDATE_CHECKER.getValue()) { + return; + } + + LOGGER.info("Checking mod updates..."); + Util.getMainWorkerExecutor().execute(UpdateCheckerUtil::checkForModrinthUpdates); + checkForCustomUpdates(); + } + + public static void checkForCustomUpdates() { + ModMenu.MODS.values().stream().filter(UpdateCheckerUtil::allowsUpdateChecks).forEach(mod -> { + UpdateChecker updateChecker = mod.getUpdateChecker(); + if (updateChecker == null) { + return; + } + UpdateCheckerThread.run(mod, () -> mod.setUpdateInfo(updateChecker.checkForUpdates())); + }); + } + + public static void checkForModrinthUpdates() { + if (modrinthApiV2Deprecated) { + return; + } + + Map> modHashes = new HashMap<>(); + new ArrayList<>(ModMenu.MODS.values()).stream().filter(UpdateCheckerUtil::allowsUpdateChecks).filter(mod -> mod.getUpdateChecker() == null).forEach(mod -> { + String modId = mod.getId(); + + try { + String hash = mod.getSha512Hash(); + + if (hash != null) { + LOGGER.debug("Hash for {} is {}", modId, hash); + modHashes.putIfAbsent(hash, new HashSet<>()); + modHashes.get(hash).add(mod); + } + } catch (IOException e) { + LOGGER.error("Error getting mod hash for mod {}: ", modId, e); + } + }); + + String environment = ModMenu.devEnvironment ? "/development" : ""; + String primaryLoader = ModMenu.runningQuilt ? "quilt" : "fabric"; + List loaders = ModMenu.runningQuilt ? List.of("fabric", "quilt") : List.of("fabric"); + + String mcVer = SharedConstants.getGameVersion().getName(); + String[] splitVersion = FabricLoader.getInstance().getModContainer(ModMenu.MOD_ID) + .get().getMetadata().getVersion().getFriendlyString().split("\\+", 1); // Strip build metadata for privacy + final var modMenuVersion = splitVersion.length > 1 ? splitVersion[1] : splitVersion[0]; + final var userAgent = "%s/%s (%s/%s%s)".formatted(ModMenu.GITHUB_REF, modMenuVersion, mcVer, primaryLoader, environment); + String body = ModMenu.GSON_MINIFIED.toJson(new LatestVersionsFromHashesBody(modHashes.keySet(), loaders, mcVer)); + LOGGER.debug("User agent: " + userAgent); + LOGGER.debug("Body: " + body); + var latestVersionsRequest = HttpRequest.newBuilder() + .POST(HttpRequest.BodyPublishers.ofString(body)) + .header("User-Agent", userAgent) + .header("Content-Type", "application/json") + .uri(URI.create("https://api.modrinth.com/v2/version_files/update")) + .build(); + + try { + var latestVersionsResponse = client.send(latestVersionsRequest, HttpResponse.BodyHandlers.ofString()); + + int status = latestVersionsResponse.statusCode(); + LOGGER.debug("Status: " + status); + if (status == 410) { + modrinthApiV2Deprecated = true; + LOGGER.warn("Modrinth API v2 is deprecated, unable to check for mod updates."); + } else if (status == 200) { + JsonObject responseObject = JsonParser.parseString(latestVersionsResponse.body()).getAsJsonObject(); + LOGGER.debug(String.valueOf(responseObject)); + responseObject.asMap().forEach((lookupHash, versionJson) -> { + var versionObj = versionJson.getAsJsonObject(); + var projectId = versionObj.get("project_id").getAsString(); + var versionNumber = versionObj.get("version_number").getAsString(); + var versionId = versionObj.get("id").getAsString(); + var primaryFile = versionObj.get("files").getAsJsonArray().asList().stream() + .filter(file -> file.getAsJsonObject().get("primary").getAsBoolean()).findFirst(); + + if (primaryFile.isEmpty()) { + return; + } + + 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)); + }); + } + }); + } + } catch (IOException | InterruptedException e) { + LOGGER.error("Error checking for updates: ", e); + } + } + + 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") + )); + } + } + + public static class LatestVersionsFromHashesBody { + public Collection hashes; + public String algorithm = "sha512"; + public Collection loaders; + @SerializedName("game_versions") + public Collection gameVersions; + + public LatestVersionsFromHashesBody(Collection hashes, Collection loaders, String mcVersion) { + this.hashes = hashes; + this.loaders = loaders; + this.gameVersions = Set.of(mcVersion); + } + } +} diff --git a/src/main/java/com/terraformersmc/modmenu/util/mod/Mod.java b/src/main/java/com/terraformersmc/modmenu/util/mod/Mod.java index 44a371ab..22c95d6a 100644 --- a/src/main/java/com/terraformersmc/modmenu/util/mod/Mod.java +++ b/src/main/java/com/terraformersmc/modmenu/util/mod/Mod.java @@ -1,16 +1,14 @@ package com.terraformersmc.modmenu.util.mod; -import com.google.common.hash.Hashing; -import com.google.common.io.Files; +import com.terraformersmc.modmenu.api.UpdateChecker; +import com.terraformersmc.modmenu.api.UpdateInfo; import com.terraformersmc.modmenu.config.ModMenuConfig; import com.terraformersmc.modmenu.util.mod.fabric.FabricIconHandler; -import net.fabricmc.loader.api.metadata.ModOrigin; import net.minecraft.client.resource.language.I18n; import net.minecraft.client.texture.NativeImageBackedTexture; import net.minecraft.text.Text; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.quiltmc.loader.api.QuiltLoader; import java.io.IOException; import java.util.*; @@ -99,17 +97,30 @@ default String getTranslatedDescription() { boolean isReal(); + boolean allowsUpdateChecks(); + @Nullable - ModrinthData getModrinthData(); + UpdateChecker getUpdateChecker(); - boolean allowsUpdateChecks(); + void setUpdateChecker(@Nullable UpdateChecker updateChecker); + + @Nullable + UpdateInfo getUpdateInfo(); + + void setUpdateInfo(@Nullable UpdateInfo updateInfo); + + default boolean hasUpdate() { + UpdateInfo updateInfo = getUpdateInfo(); + if (updateInfo == null) { + return false; + } + return updateInfo.isUpdateAvailable(); + } default @Nullable String getSha512Hash() throws IOException { return null; } - void setModrinthData(ModrinthData modrinthData); - void setChildHasUpdate(); boolean getChildHasUpdate(); diff --git a/src/main/java/com/terraformersmc/modmenu/util/mod/ModSearch.java b/src/main/java/com/terraformersmc/modmenu/util/mod/ModSearch.java index 1155cf83..95fb0c4b 100644 --- a/src/main/java/com/terraformersmc/modmenu/util/mod/ModSearch.java +++ b/src/main/java/com/terraformersmc/modmenu/util/mod/ModSearch.java @@ -6,7 +6,6 @@ import net.minecraft.client.resource.language.I18n; import net.minecraft.util.Pair; -import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.stream.Collectors; @@ -68,7 +67,7 @@ private static int passesFilters(ModsScreen screen, Mod mod, String query) { || deprecated.contains(query) && mod.getBadges().contains(Mod.Badge.DEPRECATED) // Search for deprecated mods || clientside.contains(query) && mod.getBadges().contains(Mod.Badge.CLIENT) // Search for clientside mods || configurable.contains(query) && screen.getModHasConfigScreen().get(modId) // Search for mods that can be configured - || hasUpdate.contains(query) && mod.getModrinthData() != null // Search for mods that have updates + || hasUpdate.contains(query) && mod.hasUpdate() // Search for mods that have updates ) { return 1; } diff --git a/src/main/java/com/terraformersmc/modmenu/util/mod/ModrinthData.java b/src/main/java/com/terraformersmc/modmenu/util/mod/ModrinthData.java deleted file mode 100644 index b343611e..00000000 --- a/src/main/java/com/terraformersmc/modmenu/util/mod/ModrinthData.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.terraformersmc.modmenu.util.mod; - -public record ModrinthData( - String projectId, - String versionId, - String versionNumber -) { -} diff --git a/src/main/java/com/terraformersmc/modmenu/util/mod/ModrinthUpdateInfo.java b/src/main/java/com/terraformersmc/modmenu/util/mod/ModrinthUpdateInfo.java new file mode 100644 index 00000000..9a17b5a4 --- /dev/null +++ b/src/main/java/com/terraformersmc/modmenu/util/mod/ModrinthUpdateInfo.java @@ -0,0 +1,39 @@ +package com.terraformersmc.modmenu.util.mod; + +import com.terraformersmc.modmenu.api.UpdateInfo; + +public class ModrinthUpdateInfo implements UpdateInfo { + + protected String projectId; + protected String versionId; + protected String versionNumber; + + public ModrinthUpdateInfo(String projectId, String versionId, String versionNumber) { + this.projectId = projectId; + this.versionId = versionId; + this.versionNumber = versionNumber; + } + + @Override + public boolean isUpdateAvailable() { + return true; + } + + @Override + public String getDownloadLink() { + return "https://modrinth.com/project/%s/version/%s".formatted(projectId, versionId); + } + + public String getProjectId() { + return projectId; + } + + public String getVersionId() { + return versionId; + } + + public String getVersionNumber() { + return versionNumber; + } + +} diff --git a/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricDummyParentMod.java b/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricDummyParentMod.java index 4d775eef..d7d32d73 100644 --- a/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricDummyParentMod.java +++ b/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricDummyParentMod.java @@ -1,9 +1,10 @@ package com.terraformersmc.modmenu.util.mod.fabric; import com.terraformersmc.modmenu.ModMenu; +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.ModrinthData; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.ModContainer; import net.minecraft.client.texture.NativeImageBackedTexture; @@ -157,18 +158,28 @@ public boolean isReal() { } @Override - public @Nullable ModrinthData getModrinthData() { + public boolean allowsUpdateChecks() { + return false; + } + + @Override + public @Nullable UpdateChecker getUpdateChecker() { return null; } @Override - public void setModrinthData(ModrinthData modrinthData) { - // Not a real mod, won't exist on Modrinth + public void setUpdateChecker(@Nullable UpdateChecker updateChecker) { + } @Override - public boolean allowsUpdateChecks() { - return false; + public @Nullable UpdateInfo getUpdateInfo() { + return null; + } + + @Override + public void setUpdateInfo(@Nullable UpdateInfo updateInfo) { + } @Override diff --git a/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricMod.java b/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricMod.java index b9d0dc94..506fbbc4 100644 --- a/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricMod.java +++ b/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricMod.java @@ -5,11 +5,12 @@ import com.google.common.hash.Hashing; import com.google.common.io.Files; import com.terraformersmc.modmenu.ModMenu; +import com.terraformersmc.modmenu.api.UpdateChecker; +import com.terraformersmc.modmenu.api.UpdateInfo; import com.terraformersmc.modmenu.config.ModMenuConfig; import com.terraformersmc.modmenu.util.OptionalUtil; import com.terraformersmc.modmenu.util.VersionUtil; import com.terraformersmc.modmenu.util.mod.Mod; -import com.terraformersmc.modmenu.util.mod.ModrinthData; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.ModContainer; import net.fabricmc.loader.api.metadata.*; @@ -37,7 +38,8 @@ public class FabricMod implements Mod { protected final Map links = new HashMap<>(); - protected @Nullable ModrinthData modrinthData = null; + protected @Nullable UpdateChecker updateChecker = null; + protected @Nullable UpdateInfo updateInfo = null; protected boolean defaultIconWarning = true; @@ -277,20 +279,30 @@ public boolean isReal() { } @Override - public @Nullable ModrinthData getModrinthData() { - return this.modrinthData; + public boolean allowsUpdateChecks() { + return this.allowsUpdateChecks || ModMenuConfig.DISABLE_UPDATE_CHECKER.getValue().contains(this.getId()); } @Override - public boolean allowsUpdateChecks() { - return this.allowsUpdateChecks || ModMenuConfig.DISABLE_UPDATE_CHECKER.getValue().contains(this.getId()); + public @Nullable UpdateChecker getUpdateChecker() { + return updateChecker; + } + + @Override + public void setUpdateChecker(@Nullable UpdateChecker updateChecker) { + this.updateChecker = updateChecker; + } + + @Override + public @Nullable UpdateInfo getUpdateInfo() { + return updateInfo; } @Override - public void setModrinthData(ModrinthData modrinthData) { - this.modrinthData = modrinthData; + public void setUpdateInfo(@Nullable UpdateInfo updateInfo) { + this.updateInfo = updateInfo; String parent = getParent(); - if (parent != null && modrinthData != null) { + if (parent != null && updateInfo != null && updateInfo.isUpdateAvailable()) { ModMenu.MODS.get(parent).setChildHasUpdate(); } } diff --git a/src/main/java/com/terraformersmc/modmenu/util/mod/quilt/QuiltMod.java b/src/main/java/com/terraformersmc/modmenu/util/mod/quilt/QuiltMod.java index 3b057ee1..d293d9d3 100644 --- a/src/main/java/com/terraformersmc/modmenu/util/mod/quilt/QuiltMod.java +++ b/src/main/java/com/terraformersmc/modmenu/util/mod/quilt/QuiltMod.java @@ -2,7 +2,7 @@ import com.google.common.collect.Lists; import com.google.common.hash.Hashing; -import com.terraformersmc.modmenu.util.ModrinthUtil; +import com.terraformersmc.modmenu.util.UpdateCheckerUtil; import com.terraformersmc.modmenu.util.mod.fabric.FabricMod; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -15,7 +15,6 @@ import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Set; @@ -69,7 +68,7 @@ public QuiltMod(net.fabricmc.loader.api.ModContainer fabricModContainer, Set jars = paths.stream().filter(p -> p.toString().toLowerCase(Locale.ROOT).endsWith(".jar")).toList(); @@ -78,7 +77,7 @@ public QuiltMod(net.fabricmc.loader.api.ModContainer fabricModContainer, Set Date: Thu, 18 Apr 2024 17:56:24 +0200 Subject: [PATCH 06/10] Fix disable update checks config not working for individual mods (#703) - Fixed disable_update_checker config option not working --- .../terraformersmc/modmenu/util/mod/fabric/FabricMod.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricMod.java b/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricMod.java index 506fbbc4..57acce9f 100644 --- a/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricMod.java +++ b/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricMod.java @@ -280,7 +280,11 @@ public boolean isReal() { @Override public boolean allowsUpdateChecks() { - return this.allowsUpdateChecks || ModMenuConfig.DISABLE_UPDATE_CHECKER.getValue().contains(this.getId()); + if (ModMenuConfig.DISABLE_UPDATE_CHECKER.getValue().contains(this.getId())) { + return false; + } + + return this.allowsUpdateChecks; } @Override From 5eba2466799196f906c72958c3fbaf0e43d11b17 Mon Sep 17 00:00:00 2001 From: Lilly Rose Berner Date: Mon, 22 Apr 2024 21:48:10 +0200 Subject: [PATCH 07/10] Add option to choose which update channel to receive notifications for (#704) * Allow choosing an update channel to receive notifications for * Pass user preferred update channel to UpdateChecker * Change method of getting the preferred update channel * Request preferred version types from Modrinth only * Switch update channel and quick configure button order * Rerun update checks when clicking done in the modmenu options screen - No longer promotes updates to beta or alpha versions by default --- .../com/terraformersmc/modmenu/ModMenu.java | 6 +++- .../modmenu/api/UpdateChannel.java | 19 +++++++++++ .../modmenu/api/UpdateChecker.java | 4 +-- .../modmenu/api/UpdateInfo.java | 5 ++- .../modmenu/config/ModMenuConfig.java | 2 ++ .../modmenu/gui/ModMenuOptionsScreen.java | 2 ++ .../modmenu/util/UpdateCheckerUtil.java | 33 +++++++++++++++++-- .../terraformersmc/modmenu/util/mod/Mod.java | 3 +- .../modmenu/util/mod/ModrinthUpdateInfo.java | 16 ++++++--- .../resources/assets/modmenu/lang/en_us.json | 6 +++- 10 files changed, 82 insertions(+), 14 deletions(-) create mode 100644 src/main/java/com/terraformersmc/modmenu/api/UpdateChannel.java diff --git a/src/main/java/com/terraformersmc/modmenu/ModMenu.java b/src/main/java/com/terraformersmc/modmenu/ModMenu.java index 07ddcd3a..770b7ab0 100644 --- a/src/main/java/com/terraformersmc/modmenu/ModMenu.java +++ b/src/main/java/com/terraformersmc/modmenu/ModMenu.java @@ -99,7 +99,7 @@ public void onInitializeClient() { MODS.put(mod.getId(), mod); } - UpdateCheckerUtil.checkForUpdates(); + checkForUpdates(); Map dummyParents = new HashMap<>(); @@ -127,6 +127,10 @@ public static void clearModCountCache() { cachedDisplayedModCount = -1; } + public static void checkForUpdates() { + UpdateCheckerUtil.checkForUpdates(); + } + public static boolean areModUpdatesAvailable() { if (!ModMenuConfig.UPDATE_CHECKER.getValue()) { return false; diff --git a/src/main/java/com/terraformersmc/modmenu/api/UpdateChannel.java b/src/main/java/com/terraformersmc/modmenu/api/UpdateChannel.java new file mode 100644 index 00000000..4c51835c --- /dev/null +++ b/src/main/java/com/terraformersmc/modmenu/api/UpdateChannel.java @@ -0,0 +1,19 @@ +package com.terraformersmc.modmenu.api; + +import com.terraformersmc.modmenu.config.ModMenuConfig; + +/** + * Supported update channels, in ascending order by stability. + */ +public enum UpdateChannel { + ALPHA, + BETA, + RELEASE; + + /** + * @return the user's preferred update channel. + */ + public static UpdateChannel getUserPreference() { + return ModMenuConfig.UPDATE_CHANNEL.getValue(); + } +} diff --git a/src/main/java/com/terraformersmc/modmenu/api/UpdateChecker.java b/src/main/java/com/terraformersmc/modmenu/api/UpdateChecker.java index 1c2aa231..de80fd93 100644 --- a/src/main/java/com/terraformersmc/modmenu/api/UpdateChecker.java +++ b/src/main/java/com/terraformersmc/modmenu/api/UpdateChecker.java @@ -1,13 +1,13 @@ package com.terraformersmc.modmenu.api; public interface UpdateChecker { - /** * Gets called when ModMenu is checking for updates. * This is done in a separate thread, so this call can/should be blocking. * + *

Your update checker should aim to return an update on the same or a more stable channel than the user's preference which you can get via {@link UpdateChannel#getUserPreference()}.

+ * * @return The update info */ UpdateInfo checkForUpdates(); - } diff --git a/src/main/java/com/terraformersmc/modmenu/api/UpdateInfo.java b/src/main/java/com/terraformersmc/modmenu/api/UpdateInfo.java index fee9c07b..b5f40a4f 100644 --- a/src/main/java/com/terraformersmc/modmenu/api/UpdateInfo.java +++ b/src/main/java/com/terraformersmc/modmenu/api/UpdateInfo.java @@ -4,7 +4,6 @@ import org.jetbrains.annotations.Nullable; public interface UpdateInfo { - /** * @return If an update for the mod is available. */ @@ -23,4 +22,8 @@ default Text getUpdateMessage() { */ String getDownloadLink(); + /** + * @return The update channel this update is available for. + */ + UpdateChannel getUpdateChannel(); } diff --git a/src/main/java/com/terraformersmc/modmenu/config/ModMenuConfig.java b/src/main/java/com/terraformersmc/modmenu/config/ModMenuConfig.java index bdeba118..038c419e 100644 --- a/src/main/java/com/terraformersmc/modmenu/config/ModMenuConfig.java +++ b/src/main/java/com/terraformersmc/modmenu/config/ModMenuConfig.java @@ -1,6 +1,7 @@ package com.terraformersmc.modmenu.config; import com.google.gson.annotations.SerializedName; +import com.terraformersmc.modmenu.api.UpdateChannel; import com.terraformersmc.modmenu.config.option.BooleanConfigOption; import com.terraformersmc.modmenu.config.option.EnumConfigOption; import com.terraformersmc.modmenu.config.option.OptionConvertable; @@ -43,6 +44,7 @@ public class ModMenuConfig { public static final StringSetConfigOption DISABLE_UPDATE_CHECKER = new StringSetConfigOption("disable_update_checker", new HashSet<>()); public static final BooleanConfigOption UPDATE_CHECKER = new BooleanConfigOption("update_checker", true); public static final BooleanConfigOption BUTTON_UPDATE_BADGE = new BooleanConfigOption("button_update_badge", true); + public static final EnumConfigOption UPDATE_CHANNEL = new EnumConfigOption<>("update_channel", UpdateChannel.RELEASE); public static final BooleanConfigOption QUICK_CONFIGURE = new BooleanConfigOption("quick_configure", true); public static SimpleOption[] asOptions() { diff --git a/src/main/java/com/terraformersmc/modmenu/gui/ModMenuOptionsScreen.java b/src/main/java/com/terraformersmc/modmenu/gui/ModMenuOptionsScreen.java index 3e9c4a75..b0efeb3b 100644 --- a/src/main/java/com/terraformersmc/modmenu/gui/ModMenuOptionsScreen.java +++ b/src/main/java/com/terraformersmc/modmenu/gui/ModMenuOptionsScreen.java @@ -1,5 +1,6 @@ package com.terraformersmc.modmenu.gui; +import com.terraformersmc.modmenu.ModMenu; import com.terraformersmc.modmenu.config.ModMenuConfig; import com.terraformersmc.modmenu.config.ModMenuConfigManager; import net.minecraft.client.MinecraftClient; @@ -29,6 +30,7 @@ protected void init() { this.addSelectableChild(this.list); this.addDrawableChild( ButtonWidget.builder(ScreenTexts.DONE, (button) -> { + ModMenu.checkForUpdates(); ModMenuConfigManager.save(); this.client.setScreen(this.previous); }).position(this.width / 2 - 100, this.height - 27) diff --git a/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerUtil.java b/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerUtil.java index 509c2060..ce4ef7ce 100644 --- a/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerUtil.java +++ b/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerUtil.java @@ -4,6 +4,7 @@ import com.google.gson.JsonParser; import com.google.gson.annotations.SerializedName; import com.terraformersmc.modmenu.ModMenu; +import com.terraformersmc.modmenu.api.UpdateChannel; import com.terraformersmc.modmenu.api.UpdateChecker; import com.terraformersmc.modmenu.config.ModMenuConfig; import com.terraformersmc.modmenu.util.mod.Mod; @@ -85,7 +86,20 @@ public static void checkForModrinthUpdates() { .get().getMetadata().getVersion().getFriendlyString().split("\\+", 1); // Strip build metadata for privacy final var modMenuVersion = splitVersion.length > 1 ? splitVersion[1] : splitVersion[0]; final var userAgent = "%s/%s (%s/%s%s)".formatted(ModMenu.GITHUB_REF, modMenuVersion, mcVer, primaryLoader, environment); - String body = ModMenu.GSON_MINIFIED.toJson(new LatestVersionsFromHashesBody(modHashes.keySet(), loaders, mcVer)); + + List updateChannels; + UpdateChannel preferredChannel = UpdateChannel.getUserPreference(); + + if (preferredChannel == UpdateChannel.RELEASE) { + updateChannels = List.of(UpdateChannel.RELEASE); + } else if (preferredChannel == UpdateChannel.BETA) { + updateChannels = List.of(UpdateChannel.BETA, UpdateChannel.RELEASE); + } else { + updateChannels = List.of(UpdateChannel.ALPHA, UpdateChannel.BETA, UpdateChannel.RELEASE); + } + + String body = ModMenu.GSON_MINIFIED.toJson(new LatestVersionsFromHashesBody(modHashes.keySet(), loaders, mcVer, updateChannels)); + LOGGER.debug("User agent: " + userAgent); LOGGER.debug("Body: " + body); var latestVersionsRequest = HttpRequest.newBuilder() @@ -109,6 +123,7 @@ public static void checkForModrinthUpdates() { responseObject.asMap().forEach((lookupHash, versionJson) -> { var versionObj = versionJson.getAsJsonObject(); var projectId = versionObj.get("project_id").getAsString(); + var versionType = versionObj.get("version_type").getAsString(); var versionNumber = versionObj.get("version_number").getAsString(); var versionId = versionObj.get("id").getAsString(); var primaryFile = versionObj.get("files").getAsJsonArray().asList().stream() @@ -118,13 +133,14 @@ public static void checkForModrinthUpdates() { 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)); + mod.setUpdateInfo(new ModrinthUpdateInfo(projectId, versionId, versionNumber, updateChannel)); }); } }); @@ -134,6 +150,14 @@ public static void checkForModrinthUpdates() { } } + private static UpdateChannel getUpdateChannel(String versionType) { + try { + return UpdateChannel.valueOf(versionType.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException | NullPointerException e) { + return UpdateChannel.RELEASE; + } + } + public static void triggerV2DeprecatedToast() { if (modrinthApiV2Deprecated && ModMenuConfig.UPDATE_CHECKER.getValue()) { MinecraftClient.getInstance().getToastManager().add(new SystemToast( @@ -150,11 +174,14 @@ public static class LatestVersionsFromHashesBody { public Collection loaders; @SerializedName("game_versions") public Collection gameVersions; + @SerializedName("version_types") + public Collection versionTypes; - public LatestVersionsFromHashesBody(Collection hashes, Collection loaders, String mcVersion) { + public LatestVersionsFromHashesBody(Collection hashes, Collection loaders, String mcVersion, Collection updateChannels) { this.hashes = hashes; this.loaders = loaders; this.gameVersions = Set.of(mcVersion); + this.versionTypes = updateChannels.stream().map(value -> value.toString().toLowerCase()).toList(); } } } diff --git a/src/main/java/com/terraformersmc/modmenu/util/mod/Mod.java b/src/main/java/com/terraformersmc/modmenu/util/mod/Mod.java index 22c95d6a..58e2ec4d 100644 --- a/src/main/java/com/terraformersmc/modmenu/util/mod/Mod.java +++ b/src/main/java/com/terraformersmc/modmenu/util/mod/Mod.java @@ -114,7 +114,8 @@ default boolean hasUpdate() { if (updateInfo == null) { return false; } - return updateInfo.isUpdateAvailable(); + + return updateInfo.isUpdateAvailable() && updateInfo.getUpdateChannel().compareTo(ModMenuConfig.UPDATE_CHANNEL.getValue()) >= 0; } default @Nullable String getSha512Hash() throws IOException { diff --git a/src/main/java/com/terraformersmc/modmenu/util/mod/ModrinthUpdateInfo.java b/src/main/java/com/terraformersmc/modmenu/util/mod/ModrinthUpdateInfo.java index 9a17b5a4..f982211b 100644 --- a/src/main/java/com/terraformersmc/modmenu/util/mod/ModrinthUpdateInfo.java +++ b/src/main/java/com/terraformersmc/modmenu/util/mod/ModrinthUpdateInfo.java @@ -1,17 +1,19 @@ package com.terraformersmc.modmenu.util.mod; +import com.terraformersmc.modmenu.api.UpdateChannel; import com.terraformersmc.modmenu.api.UpdateInfo; public class ModrinthUpdateInfo implements UpdateInfo { + protected final String projectId; + protected final String versionId; + protected final String versionNumber; + protected final UpdateChannel updateChannel; - protected String projectId; - protected String versionId; - protected String versionNumber; - - public ModrinthUpdateInfo(String projectId, String versionId, String versionNumber) { + public ModrinthUpdateInfo(String projectId, String versionId, String versionNumber, UpdateChannel updateChannel) { this.projectId = projectId; this.versionId = versionId; this.versionNumber = versionNumber; + this.updateChannel = updateChannel; } @Override @@ -36,4 +38,8 @@ public String getVersionNumber() { return versionNumber; } + @Override + public UpdateChannel getUpdateChannel() { + return this.updateChannel; + } } diff --git a/src/main/resources/assets/modmenu/lang/en_us.json b/src/main/resources/assets/modmenu/lang/en_us.json index 1c42d9fb..0d6f80b0 100644 --- a/src/main/resources/assets/modmenu/lang/en_us.json +++ b/src/main/resources/assets/modmenu/lang/en_us.json @@ -164,5 +164,9 @@ "option.modmenu.button_update_badge.false": "Hidden", "option.modmenu.quick_configure": "Quick Configure", "option.modmenu.quick_configure.true": "Enabled", - "option.modmenu.quick_configure.false": "Disabled" + "option.modmenu.quick_configure.false": "Disabled", + "option.modmenu.update_channel": "Update Channel", + "option.modmenu.update_channel.alpha": "All", + "option.modmenu.update_channel.beta": "Release & Beta", + "option.modmenu.update_channel.release": "Release" } From cf58ff05054f83d314b0123916454ea9af03b877 Mon Sep 17 00:00:00 2001 From: Lilly Rose Berner Date: Mon, 22 Apr 2024 21:53:31 +0200 Subject: [PATCH 08/10] Group mod credits by role instead of bunching them together (#706) * Display credits by role instead of bunching them together * Add newlines between credit groups, allow translating role names * Fix minor code style issues * Improve translation key generation and fallback role names * Sort contributors within roles * Fix Fabric authors being credited wrong - Group mod credits together by role --- .../gui/widget/DescriptionListWidget.java | 39 +++++++++++++++++-- .../terraformersmc/modmenu/util/mod/Mod.java | 10 ++++- .../util/mod/fabric/FabricDummyParentMod.java | 8 ++-- .../modmenu/util/mod/fabric/FabricMod.java | 37 ++++++++++++------ .../modmenu/util/mod/quilt/QuiltMod.java | 35 +++++++++++++---- .../resources/assets/modmenu/lang/en_us.json | 8 ++++ 6 files changed, 109 insertions(+), 28 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 b600be89..c1993f1b 100644 --- a/src/main/java/com/terraformersmc/modmenu/gui/widget/DescriptionListWidget.java +++ b/src/main/java/com/terraformersmc/modmenu/gui/widget/DescriptionListWidget.java @@ -22,7 +22,6 @@ import net.minecraft.client.gui.widget.ElementListWidget; import net.minecraft.client.gui.widget.EntryListWidget; import net.minecraft.client.render.*; -import net.minecraft.client.util.math.MatrixStack; import net.minecraft.text.OrderedText; import net.minecraft.text.Text; import net.minecraft.util.Formatting; @@ -207,7 +206,8 @@ public void renderList(DrawContext DrawContext, int mouseX, int mouseY, float de children().add(new MojangCreditsEntry(line)); } } else if (!"java".equals(mod.getId())) { - List credits = mod.getCredits(); + var credits = mod.getCredits(); + if (!credits.isEmpty()) { children().add(emptyEntry); @@ -215,12 +215,31 @@ public void renderList(DrawContext DrawContext, int mouseX, int mouseY, float de children().add(new DescriptionEntry(line)); } - for (String credit : credits) { + var iterator = credits.entrySet().iterator(); + + while (iterator.hasNext()) { int indent = 8; - for (OrderedText line : textRenderer.wrapLines(Text.literal(credit), wrapWidth - 16)) { + + var role = iterator.next(); + var roleName = role.getKey(); + + for (var line : textRenderer.wrapLines(this.creditsRoleText(roleName), wrapWidth - 16)) { children().add(new DescriptionEntry(line, indent)); indent = 16; } + + for (var contributor : role.getValue()) { + indent = 16; + + for (var line : textRenderer.wrapLines(Text.literal(contributor), wrapWidth - 24)) { + children().add(new DescriptionEntry(line, indent)); + indent = 24; + } + } + + if (iterator.hasNext()) { + children().add(emptyEntry); + } } } } @@ -331,6 +350,18 @@ public void renderScrollBar(BufferBuilder bufferBuilder, Tessellator tessellator } } + private Text creditsRoleText(String roleName) { + // Replace spaces and dashes in role names with underscores if they exist + // Notably Quilted Fabric API does this with FabricMC as "Upstream Owner" + var translationKey = roleName.replaceAll("[\s-]", "_"); + + // Add an s to the default untranslated string if it ends in r since this + // Fixes common role names people use in English (e.g. Author -> Authors) + var fallback = roleName.endsWith("r") ? roleName + "s" : roleName; + + return Text.translatableWithFallback("modmenu.credits.role." + translationKey, fallback).append(Text.literal(":")); + } + protected class DescriptionEntry extends ElementListWidget.Entry { protected OrderedText text; protected int indent; diff --git a/src/main/java/com/terraformersmc/modmenu/util/mod/Mod.java b/src/main/java/com/terraformersmc/modmenu/util/mod/Mod.java index 58e2ec4d..268b3a08 100644 --- a/src/main/java/com/terraformersmc/modmenu/util/mod/Mod.java +++ b/src/main/java/com/terraformersmc/modmenu/util/mod/Mod.java @@ -68,11 +68,17 @@ default String getTranslatedDescription() { @NotNull List getAuthors(); + /** + * @return a mapping of contributors to their roles. + */ @NotNull - List getContributors(); + Map> getContributors(); + /** + * @return a mapping of roles to each contributor with that role. + */ @NotNull - List getCredits(); + SortedMap> getCredits(); @NotNull Set getBadges(); diff --git a/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricDummyParentMod.java b/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricDummyParentMod.java index d7d32d73..39e3ac26 100644 --- a/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricDummyParentMod.java +++ b/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricDummyParentMod.java @@ -89,13 +89,13 @@ public FabricDummyParentMod(FabricMod host, String id) { } @Override - public @NotNull List getContributors() { - return new ArrayList<>(); + public @NotNull Map> getContributors() { + return Map.of(); } @Override - public @NotNull List getCredits() { - return new ArrayList<>(); + public @NotNull SortedMap> getCredits() { + return new TreeMap<>(); } @Override diff --git a/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricMod.java b/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricMod.java index 57acce9f..72da944e 100644 --- a/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricMod.java +++ b/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricMod.java @@ -211,20 +211,35 @@ public FabricMod(ModContainer modContainer, Set modpackMods) { } @Override - public @NotNull List getContributors() { - List authors = metadata.getContributors().stream().map(Person::getName).collect(Collectors.toList()); - if ("minecraft".equals(getId()) && authors.isEmpty()) { - return Lists.newArrayList(); + public @NotNull Map> getContributors() { + Map> contributors = new HashMap<>(); + + for (var contributor : this.metadata.getContributors()) { + contributors.put(contributor.getName(), List.of("Contributor")); } - return authors; + + return contributors; } - @NotNull - public List getCredits() { - List list = new ArrayList<>(); - list.addAll(getAuthors()); - list.addAll(getContributors()); - return list; + @Override + public @NotNull SortedMap> getCredits() { + SortedMap> credits = new TreeMap<>(); + + var authors = this.getAuthors(); + var contributors = this.getContributors(); + + for (var author : authors) { + contributors.put(author, List.of("Author")); + } + + for (var contributor : contributors.entrySet()) { + for (var role : contributor.getValue()) { + credits.computeIfAbsent(role, key -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER)); + credits.get(role).add(contributor.getKey()); + } + } + + return credits; } @Override diff --git a/src/main/java/com/terraformersmc/modmenu/util/mod/quilt/QuiltMod.java b/src/main/java/com/terraformersmc/modmenu/util/mod/quilt/QuiltMod.java index d293d9d3..ae69ba2e 100644 --- a/src/main/java/com/terraformersmc/modmenu/util/mod/quilt/QuiltMod.java +++ b/src/main/java/com/terraformersmc/modmenu/util/mod/quilt/QuiltMod.java @@ -15,9 +15,17 @@ import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; import java.util.stream.Collectors; public class QuiltMod extends FabricMod { @@ -51,17 +59,30 @@ public QuiltMod(net.fabricmc.loader.api.ModContainer fabricModContainer, Set getContributors() { - List authors = metadata.contributors().stream().map(modContributor -> modContributor.name() + " (" + modContributor.role() + ")").collect(Collectors.toList()); - if ("minecraft".equals(getId()) && authors.isEmpty()) { - return Lists.newArrayList(); + public @NotNull Map> getContributors() { + Map> contributors = new HashMap<>(); + + for (var contributor : this.metadata.contributors()) { + contributors.put(contributor.name(), contributor.roles()); } - return authors; + + return contributors; } @Override - public @NotNull List getCredits() { - return this.getContributors(); + public @NotNull SortedMap> getCredits() { + SortedMap> credits = new TreeMap<>(); + + var contributors = this.getContributors(); + + for (var contributor : contributors.entrySet()) { + for (var role : contributor.getValue()) { + credits.computeIfAbsent(role, key -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER)); + credits.get(role).add(contributor.getKey()); + } + } + + return credits; } diff --git a/src/main/resources/assets/modmenu/lang/en_us.json b/src/main/resources/assets/modmenu/lang/en_us.json index 0d6f80b0..7e4544ff 100644 --- a/src/main/resources/assets/modmenu/lang/en_us.json +++ b/src/main/resources/assets/modmenu/lang/en_us.json @@ -87,6 +87,14 @@ "modmenu.wiki": "Wiki", "modmenu.youtube": "YouTube", + "modmenu.credits.role.author": "Authors", + "modmenu.credits.role.contributor": "Contributors", + "modmenu.credits.role.translator": "Translators", + "modmenu.credits.role.maintainer": "Maintainers", + "modmenu.credits.role.playtester": "Playtesters", + "modmenu.credits.role.illustrator": "Illustrators", + "modmenu.credits.role.owner": "Owners", + "modmenu.modsFolder": "Open Mods Folder", "modmenu.configsFolder": "Open Configs Folder", From 00fe1adf2ef65059215c6a7113b3a3d17abf563c Mon Sep 17 00:00:00 2001 From: Lilly Rose Berner Date: Mon, 22 Apr 2024 21:54:09 +0200 Subject: [PATCH 09/10] Fix user agent stripping not working (#707) - Fix Mod Menu build metadata not being properly stripped from the User Agent string generation for privacy --- .../com/terraformersmc/modmenu/util/UpdateCheckerUtil.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerUtil.java b/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerUtil.java index ce4ef7ce..f08a575e 100644 --- a/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerUtil.java +++ b/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerUtil.java @@ -82,9 +82,9 @@ public static void checkForModrinthUpdates() { List loaders = ModMenu.runningQuilt ? List.of("fabric", "quilt") : List.of("fabric"); String mcVer = SharedConstants.getGameVersion().getName(); - String[] splitVersion = FabricLoader.getInstance().getModContainer(ModMenu.MOD_ID) - .get().getMetadata().getVersion().getFriendlyString().split("\\+", 1); // Strip build metadata for privacy - final var modMenuVersion = splitVersion.length > 1 ? splitVersion[1] : splitVersion[0]; + String version = FabricLoader.getInstance().getModContainer(ModMenu.MOD_ID) + .get().getMetadata().getVersion().getFriendlyString(); + final var modMenuVersion = version.split("\\+", 2)[0]; // Strip build metadata for privacy final var userAgent = "%s/%s (%s/%s%s)".formatted(ModMenu.GITHUB_REF, modMenuVersion, mcVer, primaryLoader, environment); List updateChannels; From 77f63a44a00a8d08c447ef1199a96d299236d4fe Mon Sep 17 00:00:00 2001 From: Lilly Rose Berner Date: Mon, 22 Apr 2024 22:17:08 +0200 Subject: [PATCH 10/10] Add provided update checkers, loader update checkers (#711) - Add update checkers for Fabric Loader and Quilt Loader - Add API for mods to provide update checkers for other mods --- .../com/terraformersmc/modmenu/ModMenu.java | 10 +- .../modmenu/ModMenuModMenuCompat.java | 17 +- .../modmenu/api/ModMenuApi.java | 13 +- .../terraformersmc/modmenu/util/HttpUtil.java | 44 +++++ .../terraformersmc/modmenu/util/JsonUtil.java | 38 +++++ .../modmenu/util/UpdateCheckerUtil.java | 36 ++-- .../modmenu/util/VersionUtil.java | 8 +- .../mod/fabric/FabricLoaderUpdateChecker.java | 147 ++++++++++++++++ .../modmenu/util/mod/fabric/FabricMod.java | 2 +- .../mod/quilt/QuiltLoaderUpdateChecker.java | 157 ++++++++++++++++++ 10 files changed, 444 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/terraformersmc/modmenu/util/HttpUtil.java create mode 100644 src/main/java/com/terraformersmc/modmenu/util/JsonUtil.java create mode 100644 src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricLoaderUpdateChecker.java create mode 100644 src/main/java/com/terraformersmc/modmenu/util/mod/quilt/QuiltLoaderUpdateChecker.java diff --git a/src/main/java/com/terraformersmc/modmenu/ModMenu.java b/src/main/java/com/terraformersmc/modmenu/ModMenu.java index 770b7ab0..3ff02381 100644 --- a/src/main/java/com/terraformersmc/modmenu/ModMenu.java +++ b/src/main/java/com/terraformersmc/modmenu/ModMenu.java @@ -70,6 +70,8 @@ public void onInitializeClient() { ModMenuConfigManager.initializeConfig(); Set modpackMods = new HashSet<>(); Map updateCheckers = new HashMap<>(); + Map providedUpdateCheckers = new HashMap<>(); + FabricLoader.getInstance().getEntrypointContainers("modmenu", ModMenuApi.class).forEach(entrypoint -> { ModMetadata metadata = entrypoint.getProvider().getMetadata(); String modId = metadata.getId(); @@ -78,6 +80,7 @@ public void onInitializeClient() { configScreenFactories.put(modId, api.getModConfigScreenFactory()); apiImplementations.add(api); updateCheckers.put(modId, api.getUpdateChecker()); + providedUpdateCheckers.putAll(api.getProvidedUpdateCheckers()); api.attachModpackBadges(modpackMods::add); } catch (Throwable e) { LOGGER.error("Mod {} provides a broken implementation of ModMenuApi", modId, e); @@ -94,9 +97,14 @@ public void onInitializeClient() { mod = new FabricMod(modContainer, modpackMods); } - mod.setUpdateChecker(updateCheckers.get(mod.getId())); + var updateChecker = updateCheckers.get(mod.getId()); + + if (updateChecker == null) { + updateChecker = providedUpdateCheckers.get(mod.getId()); + } MODS.put(mod.getId(), mod); + mod.setUpdateChecker(updateChecker); } checkForUpdates(); diff --git a/src/main/java/com/terraformersmc/modmenu/ModMenuModMenuCompat.java b/src/main/java/com/terraformersmc/modmenu/ModMenuModMenuCompat.java index 99c8cacf..34c4be77 100644 --- a/src/main/java/com/terraformersmc/modmenu/ModMenuModMenuCompat.java +++ b/src/main/java/com/terraformersmc/modmenu/ModMenuModMenuCompat.java @@ -1,16 +1,18 @@ package com.terraformersmc.modmenu; -import com.google.common.collect.ImmutableMap; import com.terraformersmc.modmenu.api.ConfigScreenFactory; import com.terraformersmc.modmenu.api.ModMenuApi; +import com.terraformersmc.modmenu.api.UpdateChecker; import com.terraformersmc.modmenu.gui.ModMenuOptionsScreen; +import com.terraformersmc.modmenu.util.mod.fabric.FabricLoaderUpdateChecker; +import com.terraformersmc.modmenu.util.mod.quilt.QuiltLoaderUpdateChecker; + import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.screen.option.OptionsScreen; import java.util.Map; public class ModMenuModMenuCompat implements ModMenuApi { - @Override public ConfigScreenFactory getModConfigScreenFactory() { return ModMenuOptionsScreen::new; @@ -18,6 +20,15 @@ public ConfigScreenFactory getModConfigScreenFactory() { @Override public Map> getProvidedConfigScreenFactories() { - return ImmutableMap.of("minecraft", parent -> new OptionsScreen(parent, MinecraftClient.getInstance().options)); + return Map.of("minecraft", parent -> new OptionsScreen(parent, MinecraftClient.getInstance().options)); + } + + @Override + public Map getProvidedUpdateCheckers() { + if (ModMenu.runningQuilt) { + return Map.of("quilt_loader", new QuiltLoaderUpdateChecker()); + } else { + return Map.of("fabricloader", new FabricLoaderUpdateChecker()); + } } } diff --git a/src/main/java/com/terraformersmc/modmenu/api/ModMenuApi.java b/src/main/java/com/terraformersmc/modmenu/api/ModMenuApi.java index 4a6bc27b..729209f8 100644 --- a/src/main/java/com/terraformersmc/modmenu/api/ModMenuApi.java +++ b/src/main/java/com/terraformersmc/modmenu/api/ModMenuApi.java @@ -1,6 +1,5 @@ package com.terraformersmc.modmenu.api; -import com.google.common.collect.ImmutableMap; import com.terraformersmc.modmenu.ModMenu; import com.terraformersmc.modmenu.gui.ModsScreen; import net.minecraft.client.gui.screen.Screen; @@ -66,7 +65,17 @@ default UpdateChecker getUpdateChecker() { * @return a map of mod ids to screen factories. */ default Map> getProvidedConfigScreenFactories() { - return ImmutableMap.of(); + return Map.of(); + } + + /** + * Used to provide update checkers for other mods. A mod registering its own + * update checker will take priority over any provided ones should both exist. + * + * @return a map of mod ids to update checkers. + */ + default Map getProvidedUpdateCheckers() { + return Map.of(); } /** diff --git a/src/main/java/com/terraformersmc/modmenu/util/HttpUtil.java b/src/main/java/com/terraformersmc/modmenu/util/HttpUtil.java new file mode 100644 index 00000000..bf91b600 --- /dev/null +++ b/src/main/java/com/terraformersmc/modmenu/util/HttpUtil.java @@ -0,0 +1,44 @@ +package com.terraformersmc.modmenu.util; + +import java.io.IOException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +import com.terraformersmc.modmenu.ModMenu; + +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.SharedConstants; + +public class HttpUtil { + private static final String USER_AGENT = buildUserAgent(); + private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient(); + + private HttpUtil() {} + + public static HttpResponse request(HttpRequest.Builder builder, HttpResponse.BodyHandler handler) throws IOException, InterruptedException { + builder.setHeader("User-Agent", USER_AGENT); + return HTTP_CLIENT.send(builder.build(), handler); + } + + private static String buildUserAgent() { + String env = ModMenu.devEnvironment ? "/development" : ""; + String loader = ModMenu.runningQuilt ? "quilt" : "fabric"; + + var modMenuVersion = getModMenuVersion(); + var minecraftVersion = SharedConstants.getGameVersion().getName(); + + // -> TerraformersMC/ModMenu/9.1.0 (1.20.3/quilt/development) + return "%s/%s (%s/%s%s)".formatted(ModMenu.GITHUB_REF, modMenuVersion, minecraftVersion, loader, env); + } + + private static String getModMenuVersion() { + var container = FabricLoader.getInstance().getModContainer(ModMenu.MOD_ID); + + if (container.isEmpty()) { + throw new RuntimeException("Unable to find Modmenu's own mod container!"); + } + + return VersionUtil.removeBuildMetadata(container.get().getMetadata().getVersion().getFriendlyString()); + } +} diff --git a/src/main/java/com/terraformersmc/modmenu/util/JsonUtil.java b/src/main/java/com/terraformersmc/modmenu/util/JsonUtil.java new file mode 100644 index 00000000..4a5e7b2d --- /dev/null +++ b/src/main/java/com/terraformersmc/modmenu/util/JsonUtil.java @@ -0,0 +1,38 @@ +package com.terraformersmc.modmenu.util; + +import java.util.Optional; + +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +public class JsonUtil { + private JsonUtil() {} + + public static Optional getString(JsonObject parent, String field) { + if (!parent.has(field)) { + return Optional.empty(); + } + + var value = parent.get(field); + + if (!value.isJsonPrimitive() || !((JsonPrimitive)value).isString()) { + return Optional.empty(); + } + + return Optional.of(value.getAsString()); + } + + public static Optional getBoolean(JsonObject parent, String field) { + if (!parent.has(field)) { + return Optional.empty(); + } + + var value = parent.get(field); + + if (!value.isJsonPrimitive() || !((JsonPrimitive)value).isBoolean()) { + return Optional.empty(); + } + + return Optional.of(value.getAsBoolean()); + } +} diff --git a/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerUtil.java b/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerUtil.java index f08a575e..a9423ae2 100644 --- a/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerUtil.java +++ b/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerUtil.java @@ -15,12 +15,12 @@ import net.minecraft.client.toast.SystemToast; import net.minecraft.text.Text; import net.minecraft.util.Util; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.URI; -import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.*; @@ -28,7 +28,6 @@ public class UpdateCheckerUtil { public static final Logger LOGGER = LoggerFactory.getLogger("Mod Menu/Update Checker"); - private static final HttpClient client = HttpClient.newHttpClient(); private static boolean modrinthApiV2Deprecated = false; private static boolean allowsUpdateChecks(Mod mod) { @@ -48,10 +47,21 @@ public static void checkForUpdates() { public static void checkForCustomUpdates() { ModMenu.MODS.values().stream().filter(UpdateCheckerUtil::allowsUpdateChecks).forEach(mod -> { UpdateChecker updateChecker = mod.getUpdateChecker(); + if (updateChecker == null) { return; } - UpdateCheckerThread.run(mod, () -> mod.setUpdateInfo(updateChecker.checkForUpdates())); + + UpdateCheckerThread.run(mod, () -> { + var update = updateChecker.checkForUpdates(); + + if (update == null) { + return; + } + + mod.setUpdateInfo(update); + LOGGER.info("Update available for '{}@{}'", mod.getId(), mod.getVersion()); + }); }); } @@ -77,15 +87,8 @@ public static void checkForModrinthUpdates() { } }); - String environment = ModMenu.devEnvironment ? "/development" : ""; - String primaryLoader = ModMenu.runningQuilt ? "quilt" : "fabric"; - List loaders = ModMenu.runningQuilt ? List.of("fabric", "quilt") : List.of("fabric"); - String mcVer = SharedConstants.getGameVersion().getName(); - String version = FabricLoader.getInstance().getModContainer(ModMenu.MOD_ID) - .get().getMetadata().getVersion().getFriendlyString(); - final var modMenuVersion = version.split("\\+", 2)[0]; // Strip build metadata for privacy - final var userAgent = "%s/%s (%s/%s%s)".formatted(ModMenu.GITHUB_REF, modMenuVersion, mcVer, primaryLoader, environment); + List loaders = ModMenu.runningQuilt ? List.of("fabric", "quilt") : List.of("fabric"); List updateChannels; UpdateChannel preferredChannel = UpdateChannel.getUserPreference(); @@ -100,20 +103,17 @@ public static void checkForModrinthUpdates() { String body = ModMenu.GSON_MINIFIED.toJson(new LatestVersionsFromHashesBody(modHashes.keySet(), loaders, mcVer, updateChannels)); - LOGGER.debug("User agent: " + userAgent); - LOGGER.debug("Body: " + body); + LOGGER.debug("Body: {}", body); var latestVersionsRequest = HttpRequest.newBuilder() .POST(HttpRequest.BodyPublishers.ofString(body)) - .header("User-Agent", userAgent) .header("Content-Type", "application/json") - .uri(URI.create("https://api.modrinth.com/v2/version_files/update")) - .build(); + .uri(URI.create("https://api.modrinth.com/v2/version_files/update")); try { - var latestVersionsResponse = client.send(latestVersionsRequest, HttpResponse.BodyHandlers.ofString()); + var latestVersionsResponse = HttpUtil.request(latestVersionsRequest, HttpResponse.BodyHandlers.ofString()); int status = latestVersionsResponse.statusCode(); - LOGGER.debug("Status: " + status); + LOGGER.debug("Status: {}", status); if (status == 410) { modrinthApiV2Deprecated = true; LOGGER.warn("Modrinth API v2 is deprecated, unable to check for mod updates."); diff --git a/src/main/java/com/terraformersmc/modmenu/util/VersionUtil.java b/src/main/java/com/terraformersmc/modmenu/util/VersionUtil.java index 4d14b228..29e1a81c 100644 --- a/src/main/java/com/terraformersmc/modmenu/util/VersionUtil.java +++ b/src/main/java/com/terraformersmc/modmenu/util/VersionUtil.java @@ -5,9 +5,7 @@ public final class VersionUtil { private static final List PREFIXES = List.of("version", "ver", "v"); - private VersionUtil() { - return; - } + private VersionUtil() {} public static String stripPrefix(String version) { version = version.trim(); @@ -24,4 +22,8 @@ public static String stripPrefix(String version) { public static String getPrefixedVersion(String version) { return "v" + stripPrefix(version); } + + public static String removeBuildMetadata(String version) { + return version.split("\\+")[0]; + } } 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 new file mode 100644 index 00000000..fcaeb16e --- /dev/null +++ b/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricLoaderUpdateChecker.java @@ -0,0 +1,147 @@ +package com.terraformersmc.modmenu.util.mod.fabric; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonParser; +import com.terraformersmc.modmenu.api.UpdateChannel; +import com.terraformersmc.modmenu.api.UpdateChecker; +import com.terraformersmc.modmenu.api.UpdateInfo; +import com.terraformersmc.modmenu.util.HttpUtil; +import com.terraformersmc.modmenu.util.JsonUtil; +import com.terraformersmc.modmenu.util.OptionalUtil; + +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.SemanticVersion; +import net.fabricmc.loader.api.Version; +import net.fabricmc.loader.api.VersionParsingException; + +public class FabricLoaderUpdateChecker implements UpdateChecker { + public static final Logger LOGGER = LoggerFactory.getLogger("Mod Menu/Fabric Update Checker"); + private static final URI LOADER_VERSIONS = URI.create("https://meta.fabricmc.net/v2/versions/loader"); + + @Override + public UpdateInfo checkForUpdates() { + UpdateInfo result = null; + + try { + result = checkForUpdates0(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (IOException e) { + LOGGER.error("Failed Fabric Loader update check!", e); + } + + return result; + } + + private static UpdateInfo checkForUpdates0() throws IOException, InterruptedException { + var preferredChannel = UpdateChannel.getUserPreference(); + + var request = HttpRequest.newBuilder().GET().uri(LOADER_VERSIONS); + var response = HttpUtil.request(request, HttpResponse.BodyHandlers.ofString()); + + var status = response.statusCode(); + + if (status != 200) { + LOGGER.warn("Fabric Meta responded with a non-200 status: {}!", status); + return null; + } + + var contentType = response.headers().firstValue("Content-Type"); + + if (contentType.isEmpty() || !contentType.get().contains("application/json")) { + LOGGER.warn("Fabric Meta responded with a non-json content type, aborting loader update check!"); + return null; + } + + var data = JsonParser.parseString(response.body()); + + if (!data.isJsonArray()) { + LOGGER.warn("Received invalid data from Fabric Meta, aborting loader update check!"); + return null; + } + + SemanticVersion match = null; + boolean stableVersion = true; + + for (var child : data.getAsJsonArray()) { + if (!child.isJsonObject()) { + continue; + } + + var object = child.getAsJsonObject(); + var version = JsonUtil.getString(object, "version"); + + if (version.isEmpty()) { + continue; + } + + SemanticVersion parsed; + + try { + parsed = SemanticVersion.parse(version.get()); + } catch (VersionParsingException e) { + continue; + } + + // Why aren't betas just marked as beta in the version string ... + var stable = OptionalUtil.isPresentAndTrue(JsonUtil.getBoolean(object, "stable")); + + if (preferredChannel == UpdateChannel.RELEASE && !stable) { + continue; + } + + if (match == null || isNewer(parsed, match)) { + match = parsed; + stableVersion = stable; + } + } + + Version current = getCurrentVersion(); + + if (match == null || !isNewer(match, current)) { + LOGGER.debug("Fabric Loader is up to date."); + return null; + } + + LOGGER.debug("Fabric Loader has a matching update available!"); + return new FabricLoaderUpdateInfo(stableVersion); + } + + private static boolean isNewer(Version self, Version other) { + return self.compareTo(other) > 0; + } + + private static Version getCurrentVersion() { + return FabricLoader.getInstance().getModContainer("fabricloader").get().getMetadata().getVersion(); + } + + private static class FabricLoaderUpdateInfo implements UpdateInfo { + private final boolean isStable; + + private FabricLoaderUpdateInfo(boolean isStable) { + this.isStable = isStable; + } + + @Override + public boolean isUpdateAvailable() { + return true; + } + + @Override + public String getDownloadLink() { + return "https://fabricmc.net/use/installer"; + } + + @Override + public UpdateChannel getUpdateChannel() { + return this.isStable ? UpdateChannel.RELEASE : UpdateChannel.BETA; + } + } +} diff --git a/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricMod.java b/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricMod.java index 72da944e..2c89e9e5 100644 --- a/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricMod.java +++ b/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricMod.java @@ -51,7 +51,7 @@ public FabricMod(ModContainer modContainer, Set modpackMods) { this.container = modContainer; this.metadata = modContainer.getMetadata(); - if ("minecraft".equals(metadata.getId()) || "fabricloader".equals(metadata.getId()) || "java".equals(metadata.getId()) || "quilt_loader".equals(metadata.getId())) { + if ("minecraft".equals(metadata.getId()) || "java".equals(metadata.getId())) { allowsUpdateChecks = false; } 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 new file mode 100644 index 00000000..9b5da457 --- /dev/null +++ b/src/main/java/com/terraformersmc/modmenu/util/mod/quilt/QuiltLoaderUpdateChecker.java @@ -0,0 +1,157 @@ +package com.terraformersmc.modmenu.util.mod.quilt; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +import org.quiltmc.loader.api.QuiltLoader; +import org.quiltmc.loader.api.Version; +import org.quiltmc.loader.api.VersionFormatException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonParser; +import com.terraformersmc.modmenu.api.UpdateChannel; +import com.terraformersmc.modmenu.api.UpdateChecker; +import com.terraformersmc.modmenu.api.UpdateInfo; +import com.terraformersmc.modmenu.util.HttpUtil; +import com.terraformersmc.modmenu.util.JsonUtil; + +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"); + + @Override + public UpdateInfo checkForUpdates() { + UpdateInfo result = null; + + try { + result = checkForUpdates0(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (IOException e) { + LOGGER.error("Failed Quilt Loader update check!", e); + } + + return result; + } + + private static UpdateInfo checkForUpdates0() throws IOException, InterruptedException { + var preferredChannel = UpdateChannel.getUserPreference(); + + var request = HttpRequest.newBuilder().GET().uri(LOADER_VERSIONS); + var response = HttpUtil.request(request, HttpResponse.BodyHandlers.ofString()); + + var status = response.statusCode(); + + if (status != 200) { + LOGGER.warn("Quilt Meta responded with a non-200 status: {}!", status); + return null; + } + + var contentType = response.headers().firstValue("Content-Type"); + + if (contentType.isEmpty() || !contentType.get().contains("application/json")) { + LOGGER.warn("Quilt Meta responded with a non-json content type, aborting loader update check!"); + return null; + } + + var data = JsonParser.parseString(response.body()); + + if (!data.isJsonArray()) { + LOGGER.warn("Received invalid data from Quilt Meta, aborting loader update check!"); + return null; + } + + Version.Semantic match = null; + + for (var child : data.getAsJsonArray()) { + if (!child.isJsonObject()) { + continue; + } + + var object = child.getAsJsonObject(); + var version = JsonUtil.getString(object, "version"); + + if (version.isEmpty()) { + continue; + } + + Version.Semantic parsed; + + try { + parsed = Version.Semantic.of(version.get()); + } catch (VersionFormatException e) { + continue; + } + + if (preferredChannel == UpdateChannel.RELEASE && !parsed.preRelease().equals("")) { + continue; + } else if (preferredChannel == UpdateChannel.BETA && !isStableOrBeta(parsed.preRelease())) { + continue; + } + + if (match == null || isNewer(parsed, match)) { + match = parsed; + } + } + + Version.Semantic current = getCurrentVersion(); + + if (match == null || !isNewer(match, current)) { + LOGGER.debug("Quilt Loader is up to date."); + return null; + } + + 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); + } + + private static boolean isNewer(Version.Semantic self, Version.Semantic other) { + return self.compareTo(other) > 0; + } + + private static Version.Semantic getCurrentVersion() { + return QuiltLoader.getModContainer("quilt_loader").get().metadata().version().semantic(); + } + + private static boolean isStableOrBeta(String preRelease) { + return preRelease.isEmpty() || preRelease.startsWith("beta") || preRelease.startsWith("pre") || preRelease.startsWith("rc"); + } + + private static class QuiltLoaderUpdateInfo implements UpdateInfo { + private final UpdateChannel updateChannel; + + private QuiltLoaderUpdateInfo(UpdateChannel updateChannel) { + this.updateChannel = updateChannel; + } + + @Override + public boolean isUpdateAvailable() { + return true; + } + + @Override + public String getDownloadLink() { + return "https://quiltmc.org/en/install/client"; + } + + @Override + public UpdateChannel getUpdateChannel() { + return this.updateChannel; + } + } +}