diff --git a/src/main/java/com/terraformersmc/modmenu/ModMenu.java b/src/main/java/com/terraformersmc/modmenu/ModMenu.java index caf9506d..fbe085cd 100644 --- a/src/main/java/com/terraformersmc/modmenu/ModMenu.java +++ b/src/main/java/com/terraformersmc/modmenu/ModMenu.java @@ -6,13 +6,14 @@ 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.ModMenuConfig.GameMenuButtonStyle; import com.terraformersmc.modmenu.config.ModMenuConfig.TitleMenuButtonStyle; import com.terraformersmc.modmenu.config.ModMenuConfigManager; import com.terraformersmc.modmenu.event.ModMenuEventHandler; -import com.terraformersmc.modmenu.util.ModrinthUtil; import com.terraformersmc.modmenu.util.TranslationUtil; +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; @@ -33,7 +34,7 @@ public class ModMenu implements ClientModInitializer { public static final String MOD_ID = "modmenu"; - public static final String GITHUB_REF = "TerraformersMC/ModMenu"; + public static final String GITHUB_REF = "OrnitheMC/ModMenu"; public static final Logger LOGGER = LogManager.getLogger("Mod Menu"); public static final Gson GSON = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).setPrettyPrinting().create(); public static final Gson GSON_MINIFIED = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); @@ -42,17 +43,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) { + Map> factoryProviders = api.getProvidedConfigScreenFactories(); + if (!factoryProviders.isEmpty()) { + factoryProviders.forEach(configScreenFactories::putIfAbsent); + } } if (ModMenuConfig.HIDDEN_CONFIGS.getValue().contains(modid)) { return null; @@ -68,13 +71,18 @@ public static Screen getConfigScreen(String modid, Screen menuScreen) { public void initClient() { 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(); try { ModMenuApi api = entrypoint.getEntrypoint(); configScreenFactories.put(modId, api.getModConfigScreenFactory()); - delayedScreenFactoryProviders.add(api.getProvidedConfigScreenFactories()); + 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); @@ -91,10 +99,17 @@ public void initClient() { mod = new FabricMod(modContainer, modpackMods); } + UpdateChecker updateChecker = updateCheckers.get(mod.getId()); + + if (updateChecker == null) { + updateChecker = providedUpdateCheckers.get(mod.getId()); + } + MODS.put(mod.getId(), mod); + mod.setUpdateChecker(updateChecker); } - ModrinthUtil.checkForUpdates(); + checkForUpdates(); Map dummyParents = new HashMap<>(); @@ -122,6 +137,10 @@ public static void clearModCountCache() { cachedDisplayedModCount = -1; } + public static void checkForUpdates() { + UpdateCheckerUtil.checkForUpdates(); + } + public static boolean areModUpdatesAvailable() { if (!ModMenuConfig.UPDATE_CHECKER.getValue()) { return false; @@ -136,7 +155,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 } } @@ -148,9 +167,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); diff --git a/src/main/java/com/terraformersmc/modmenu/ModMenuModMenuCompat.java b/src/main/java/com/terraformersmc/modmenu/ModMenuModMenuCompat.java index 12e7cc86..577b016b 100644 --- a/src/main/java/com/terraformersmc/modmenu/ModMenuModMenuCompat.java +++ b/src/main/java/com/terraformersmc/modmenu/ModMenuModMenuCompat.java @@ -3,14 +3,17 @@ 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.Minecraft; import net.minecraft.client.gui.screen.options.OptionsScreen; import java.util.Map; public class ModMenuModMenuCompat implements ModMenuApi { - @Override public ConfigScreenFactory getModConfigScreenFactory() { return ModMenuOptionsScreen::new; @@ -20,4 +23,13 @@ public ConfigScreenFactory getModConfigScreenFactory() { public Map> getProvidedConfigScreenFactories() { return ImmutableMap.of("minecraft", parent -> new OptionsScreen(parent, Minecraft.getInstance().options)); } + + @Override + public Map getProvidedUpdateCheckers() { + if (ModMenu.runningQuilt) { + return ImmutableMap.of("quilt_loader", new QuiltLoaderUpdateChecker()); + } else { + return ImmutableMap.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 ef66bdc9..db1f70d7 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 @@ -59,6 +69,16 @@ default Map> getProvidedConfigScreenFactories() { return ImmutableMap.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 ImmutableMap.of(); + } + /** * Used to mark mods with a badge indicating that they are * provided by a modpack. 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 new file mode 100644 index 00000000..de80fd93 --- /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. + * + *

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 new file mode 100644 index 00000000..b5f40a4f --- /dev/null +++ b/src/main/java/com/terraformersmc/modmenu/api/UpdateInfo.java @@ -0,0 +1,29 @@ +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(); + + /** + * @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 c4fde459..7259e587 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.ConfigOption; import com.terraformersmc.modmenu.config.option.EnumConfigOption; @@ -41,6 +42,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 ConfigOption[] asOptions() { diff --git a/src/main/java/com/terraformersmc/modmenu/gui/ModMenuOptionsScreen.java b/src/main/java/com/terraformersmc/modmenu/gui/ModMenuOptionsScreen.java index d3c74d0f..9c9d96a8 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 com.terraformersmc.modmenu.gui.widget.ConfigOptionListWidget; @@ -56,6 +57,7 @@ public void mouseClicked(int mouseX, int mouseY, int button) { public void buttonClicked(ButtonWidget button) { switch (button.id) { case DONE: + ModMenu.checkForUpdates(); ModMenuConfigManager.save(); ModMenuOptionsScreen.this.minecraft.openScreen(ModMenuOptionsScreen.this.previous); break; @@ -64,6 +66,7 @@ public void buttonClicked(ButtonWidget button) { @Override public void removed() { + ModMenu.checkForUpdates(); ModMenuConfigManager.save(); } } 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 04de1d71..eaf220be 100644 --- a/src/main/java/com/terraformersmc/modmenu/gui/widget/DescriptionListWidget.java +++ b/src/main/java/com/terraformersmc/modmenu/gui/widget/DescriptionListWidget.java @@ -4,6 +4,7 @@ import com.mojang.blaze3d.vertex.BufferBuilder; import com.mojang.blaze3d.vertex.DefaultVertexFormat; import com.mojang.blaze3d.vertex.Tessellator; +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; @@ -14,19 +15,24 @@ import net.minecraft.client.gui.screen.ConfirmChatLinkScreen; import net.minecraft.client.gui.screen.ConfirmationListener; import net.minecraft.client.gui.screen.CreditsScreen; +import com.terraformersmc.modmenu.util.mod.ModrinthUpdateInfo; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.widget.EntryListWidget; import net.minecraft.client.render.*; import net.minecraft.text.Formatting; import net.minecraft.text.Style; +import net.minecraft.text.LiteralText; import net.minecraft.text.Text; import net.minecraft.text.TranslatableText; import net.minecraft.util.math.MathHelper; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; import org.lwjgl.opengl.GL11; @@ -34,7 +40,7 @@ public class DescriptionListWidget extends EntryListWidget implements Confirmati private static final Text HAS_UPDATE_TEXT = new TranslatableText("modmenu.hasUpdate"); private static final Text EXPERIMENTAL_TEXT = new TranslatableText("modmenu.experimental").setStyle(new Style().setColor(Formatting.GOLD)); - private static final Text MODRINTH_TEXT = new TranslatableText("modmenu.modrinth"); + private static final Text DOWNLOAD_TEXT = new TranslatableText("modmenu.downloadLink").setStyle(new Style().setColor(Formatting.BLUE).setUnderlined(true)); private static final Text CHILD_HAS_UPDATE_TEXT = new TranslatableText("modmenu.childHasUpdate"); private static final Text LINKS_TEXT = new TranslatableText("modmenu.links"); private static final Text SOURCE_TEXT = new TranslatableText("modmenu.source").setStyle(new Style().setColor(Formatting.BLUE).setUnderlined(true)); @@ -104,7 +110,8 @@ public void render(int mouseX, int mouseY, float delta) { } if (ModMenuConfig.UPDATE_CHECKER.getValue() && !ModMenuConfig.DISABLE_UPDATE_CHECKER.getValue().contains(mod.getId())) { - if (mod.getModrinthData() != null) { + UpdateInfo updateInfo = mod.getUpdateInfo(); + if (updateInfo != null && updateInfo.isUpdateAvailable()) { this.entries.add(emptyEntry); int index = 0; @@ -120,13 +127,21 @@ public void render(int mouseX, int mouseY, float delta) { this.entries.add(new DescriptionEntry(line, 8)); } - Text updateText = new TranslatableText("modmenu.updateText", VersionUtil.stripPrefix(mod.getModrinthData().versionNumber()), MODRINTH_TEXT) - .setStyle(new Style().setColor(Formatting.BLUE).setUnderlined(true)); - - String versionLink = String.format("https://modrinth.com/project/%s/version/%s", mod.getModrinthData().projectId(), mod.getModrinthData().versionId()); - - for (String line : textRenderer.wrapLines(updateText.getFormattedContent(), wrapWidth - 16)) { - this.entries.add(new LinkEntry(line, versionLink, 8)); + Text updateMessage = updateInfo.getUpdateMessage(); + String downloadLink = updateInfo.getDownloadLink(); + if (updateMessage == null) { + updateMessage = DOWNLOAD_TEXT; + } else { + if (downloadLink != null) { + updateMessage = updateMessage.copy().setStyle(new Style().setColor(Formatting.BLUE).setUnderlined(true)); + } + } + for (String line : textRenderer.wrapLines(updateMessage.getFormattedContent(), wrapWidth - 16)) { + if (downloadLink != null) { + this.entries.add(new LinkEntry(line, downloadLink, 8)); + } else { + this.entries.add(new DescriptionEntry(line, 8)); + } } } if (mod.getChildHasUpdate()) { @@ -194,7 +209,8 @@ public void render(int mouseX, int mouseY, float delta) { this.entries.add(new MojangCreditsEntry(line)); } } else if (!"java".equals(mod.getId())) { - List credits = mod.getCredits(); + SortedMap> credits = mod.getCredits(); + if (!credits.isEmpty()) { this.entries.add(emptyEntry); @@ -202,12 +218,31 @@ public void render(int mouseX, int mouseY, float delta) { this.entries.add(new DescriptionEntry(line)); } - for (String credit : credits) { + Iterator>> iterator = credits.entrySet().iterator(); + + while (iterator.hasNext()) { int indent = 8; - for (String line : textRenderer.wrapLines(credit, wrapWidth - 16)) { + + Map.Entry> role = iterator.next(); + String roleName = role.getKey(); + + for (String line : textRenderer.wrapLines(this.creditsRoleText(roleName).getFormattedContent(), wrapWidth - 16)) { this.entries.add(new DescriptionEntry(line, indent)); indent = 16; } + + for (String contributor : role.getValue()) { + indent = 16; + + for (String line : textRenderer.wrapLines(new LiteralText(contributor).getFormattedContent(), wrapWidth - 24)) { + this.entries.add(new DescriptionEntry(line, indent)); + indent = 24; + } + } + + if (iterator.hasNext()) { + this.entries.add(emptyEntry); + } } } } @@ -346,6 +381,14 @@ public void confirmResult(boolean result, int id) { minecraft.openScreen(this.parent); } + 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" + String translationKey = roleName.replaceAll("[\\s-]", "_").toLowerCase(); + + return new TranslatableText("modmenu.credits.role." + translationKey).append(new LiteralText(":")); + } + protected class DescriptionEntry implements EntryListWidget.Entry { protected String text; protected int indent; diff --git a/src/main/java/com/terraformersmc/modmenu/gui/widget/entries/ModListEntry.java b/src/main/java/com/terraformersmc/modmenu/gui/widget/entries/ModListEntry.java index b5501160..2fae000c 100644 --- a/src/main/java/com/terraformersmc/modmenu/gui/widget/entries/ModListEntry.java +++ b/src/main/java/com/terraformersmc/modmenu/gui/widget/entries/ModListEntry.java @@ -67,7 +67,7 @@ public void render(int index, int x, int y, int rowWidth, int rowHeight, int mou } font.drawWithoutShadow(trimmedName.getFormattedContent(), x + iconSize + 3, y + 1, 0xFFFFFF); int updateBadgeXOffset = 0; - if (ModMenuConfig.UPDATE_CHECKER.getValue() && !ModMenuConfig.DISABLE_UPDATE_CHECKER.getValue().contains(modId) && (mod.getModrinthData() != null || mod.getChildHasUpdate())) { + if (ModMenuConfig.UPDATE_CHECKER.getValue() && !ModMenuConfig.DISABLE_UPDATE_CHECKER.getValue().contains(modId) && (mod.hasUpdate() || mod.getChildHasUpdate())) { UpdateAvailableBadge.renderBadge(x + iconSize + 3 + font.getStringWidth(name.getFormattedContent()) + 2, y); updateBadgeXOffset = 11; } 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..6fa95170 --- /dev/null +++ b/src/main/java/com/terraformersmc/modmenu/util/HttpUtil.java @@ -0,0 +1,47 @@ +package com.terraformersmc.modmenu.util; + +import java.io.IOException; +import java.util.Optional; + +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; + +import com.terraformersmc.modmenu.ModMenu; + +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.ModContainer; + +public class HttpUtil { + private static final String USER_AGENT = buildUserAgent(); + private static final CloseableHttpClient HTTP_CLIENT = HttpClientBuilder.create().build(); + + private HttpUtil() {} + + public static HttpResponse request(RequestBuilder builder) throws IOException { + builder.setHeader("User-Agent", USER_AGENT); + return HTTP_CLIENT.execute(builder.build()); + } + + private static String buildUserAgent() { + String env = ModMenu.devEnvironment ? "/development" : ""; + String loader = ModMenu.runningQuilt ? "quilt" : "fabric"; + + String modMenuVersion = getModMenuVersion(ModMenu.MOD_ID); + String minecraftVersion = getModMenuVersion("minecraft"); + + // -> TerraformersMC/ModMenu/9.1.0 (1.20.3/quilt/development) + return String.format("%s/%s (%s/%s%s)", ModMenu.GITHUB_REF, modMenuVersion, minecraftVersion, loader, env); + } + + private static String getModMenuVersion(String modId) { + Optional container = FabricLoader.getInstance().getModContainer(modId); + + if (!container.isPresent()) { + 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..287b0ec5 --- /dev/null +++ b/src/main/java/com/terraformersmc/modmenu/util/JsonUtil.java @@ -0,0 +1,39 @@ +package com.terraformersmc.modmenu.util; + +import java.util.Optional; + +import com.google.gson.JsonElement; +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(); + } + + JsonElement 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(); + } + + JsonElement 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/ModrinthUtil.java b/src/main/java/com/terraformersmc/modmenu/util/ModrinthUtil.java deleted file mode 100644 index 3456b558..00000000 --- a/src/main/java/com/terraformersmc/modmenu/util/ModrinthUtil.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.terraformersmc.modmenu.util; - -import java.io.IOException; -import java.net.URI; -import java.util.*; -import java.util.concurrent.CompletableFuture; - -import com.google.gson.JsonElement; -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.config.ModMenuConfig; -import com.terraformersmc.modmenu.util.mod.Mod; -import com.terraformersmc.modmenu.util.mod.ModrinthData; -import net.fabricmc.loader.api.FabricLoader; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.client.methods.RequestBuilder; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.util.EntityUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -public class ModrinthUtil { - public static final Logger LOGGER = LogManager.getLogger("Mod Menu/Update Checker"); - - private static final HttpClient client = HttpClientBuilder.create().build(); - private static boolean apiV2Deprecated = false; - - private static boolean allowsUpdateChecks(Mod mod) { - return mod.allowsUpdateChecks(); - } - - public static void checkForUpdates() { - if (!ModMenuConfig.UPDATE_CHECKER.getValue()) { - return; - } - - CompletableFuture.runAsync(() -> { - 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 ? Arrays.asList("fabric", "quilt") : Collections.singletonList("fabric"); - - String mcVer = FabricLoader.getInstance().getModContainer("minecraft").get() - .getMetadata().getVersion().getFriendlyString(); - String[] splitVersion = FabricLoader.getInstance().getModContainer(ModMenu.MOD_ID) - .get().getMetadata().getVersion().getFriendlyString().split("\\+", 1); // Strip build metadata for privacy - final String modMenuVersion = splitVersion.length > 1 ? splitVersion[1] : splitVersion[0]; - final String userAgent = String.format("%s/%s (%s/%s%s)", 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); - - try { - HttpUriRequest latestVersionsRequest = RequestBuilder.post() - .setEntity(new StringEntity(body)) - .addHeader("User-Agent", userAgent) - .addHeader("Content-Type", "application/json") - .setUri(URI.create("https://api.modrinth.com/v2/version_files/update")) - .build(); - - HttpResponse latestVersionsResponse = client.execute(latestVersionsRequest); - - int status = latestVersionsResponse.getStatusLine().getStatusCode(); - 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 = new JsonParser().parse(EntityUtils.toString(latestVersionsResponse.getEntity())).getAsJsonObject(); - LOGGER.debug(String.valueOf(responseObject)); - responseObject.entrySet().forEach(entry -> { - String lookupHash = entry.getKey(); - JsonObject versionObj = entry.getValue().getAsJsonObject(); - String projectId = versionObj.get("project_id").getAsString(); - String versionNumber = versionObj.get("version_number").getAsString(); - String versionId = versionObj.get("id").getAsString(); - List files = new ArrayList<>(); - versionObj.get("files").getAsJsonArray().forEach(files::add); - Optional primaryFile = files.stream() - .filter(file -> file.getAsJsonObject().get("primary").getAsBoolean()).findFirst(); - - if (!primaryFile.isPresent()) { - return; - } - - String 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 e) { - LOGGER.error("Error checking for updates: ", e); - } - }); - } - - 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 = new HashSet<>(); - this.gameVersions.add(mcVersion); - } - } -} 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..e0eed63e --- /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 runnable) { + int index = COUNT.incrementAndGet(); + return new Thread(runnable, "ModMenu/Update Checker/" + index); + } +} 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..10277b36 --- /dev/null +++ b/src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerUtil.java @@ -0,0 +1,333 @@ +package com.terraformersmc.modmenu.util; + +import com.google.gson.JsonElement; +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.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 org.apache.http.HttpResponse; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.entity.StringEntity; +import org.apache.http.util.EntityUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.net.URI; +import java.time.Instant; +import java.time.format.DateTimeParseException; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +public class UpdateCheckerUtil { + public static final Logger LOGGER = LogManager.getLogger("Mod Menu/Update Checker"); + + 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..."); + CompletableFuture.runAsync(UpdateCheckerUtil::checkForUpdates0); + } + + private static void checkForUpdates0() { + ExecutorService executor = Executors.newCachedThreadPool(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/" + mod.getName()); + + UpdateInfo update = updateChecker.checkForUpdates(); + + if (update == null) { + return; + } + + mod.setUpdateInfo(update); + LOGGER.info("Update available for '{}@{}'", mod.getId(), mod.getVersion()); + }); + } + }); + + if (modrinthApiV2Deprecated) { + return; + } + + Map> modHashes = getModHashes(withoutUpdateChecker); + + Future> currentVersionsFuture = executor.submit(() -> getCurrentVersions(modHashes.keySet())); + Future> 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 (currentVersions == null || updatedVersions == null) { + return; + } + + for (String hash : modHashes.keySet()) { + Instant date = currentVersions.get(hash); + VersionUpdate data = updatedVersions.get(hash); + + if (date == null || data == null) { + continue; + } + + // Current version is still the newest + if (Objects.equals(hash, data.hash)) { + continue; + } + + // Current version is newer than what's + // Available on our preferred update channel + if (date.compareTo(data.releaseDate) >= 0) { + continue; + } + + for (Mod mod : modHashes.get(hash)) { + mod.setUpdateInfo(data.asUpdateInfo()); + LOGGER.info("Update available for '{}@{}', (-> {})", mod.getId(), mod.getVersion(), data.versionNumber); + } + } + } + + private static Map> getModHashes(Collection mods) { + Map> results = new HashMap<>(); + + for (Mod mod : mods) { + String modId = mod.getId(); + + try { + String hash = mod.getSha512Hash(); + + if (hash != null) { + LOGGER.debug("Hash for {} is {}", modId, hash); + 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; + } + + /** + * @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)); + + try { + RequestBuilder request = RequestBuilder.post() + .setEntity(new StringEntity(body)) + .addHeader("Content-Type", "application/json") + .setUri(URI.create("https://api.modrinth.com/v2/version_files")); + + HttpResponse response = HttpUtil.request(request); + int status = response.getStatusLine().getStatusCode(); + + if (status == 410) { + modrinthApiV2Deprecated = true; + LOGGER.warn("Modrinth API v2 is deprecated, unable to check for mod updates."); + } else if (status == 200) { + Map results = new HashMap<>(); + JsonObject data = new JsonParser().parse(EntityUtils.toString(response.getEntity())).getAsJsonObject(); + + data.entrySet().forEach((Map.Entry entry) -> { + Instant date; + JsonObject version = entry.getValue().getAsJsonObject(); + + try { + date = Instant.parse(version.get("date_published").getAsString()); + } catch (DateTimeParseException e) { + return; + } + + results.put(entry.getKey(), date); + }); + + return results; + } + } catch (IOException 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 = FabricLoader.getInstance().getModContainer("minecraft").get() + .getMetadata().getVersion().getFriendlyString(); + List loaders = ModMenu.runningQuilt ? Arrays.asList("fabric", "quilt") : Arrays.asList("fabric"); + + List updateChannels; + UpdateChannel preferredChannel = UpdateChannel.getUserPreference(); + + if (preferredChannel == UpdateChannel.RELEASE) { + updateChannels = Arrays.asList(UpdateChannel.RELEASE); + } else if (preferredChannel == UpdateChannel.BETA) { + updateChannels = Arrays.asList(UpdateChannel.BETA, UpdateChannel.RELEASE); + } else { + updateChannels = Arrays.asList(UpdateChannel.ALPHA, UpdateChannel.BETA, UpdateChannel.RELEASE); + } + + String body = ModMenu.GSON_MINIFIED.toJson(new LatestVersionsFromHashesBody(modHashes, loaders, mcVer, updateChannels)); + + LOGGER.debug("Body: " + body); + + try { + RequestBuilder latestVersionsRequest = RequestBuilder.post() + .setEntity(new StringEntity(body)) + .addHeader("Content-Type", "application/json") + .setUri(URI.create("https://api.modrinth.com/v2/version_files/update")); + + HttpResponse response = HttpUtil.request(latestVersionsRequest); + + int status = response.getStatusLine().getStatusCode(); + 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) { + Map results = new HashMap<>(); + JsonObject responseObject = new JsonParser().parse(EntityUtils.toString(response.getEntity())).getAsJsonObject(); + LOGGER.debug(String.valueOf(responseObject)); + responseObject.entrySet().forEach(entry -> { + String lookupHash = entry.getKey(); + JsonObject versionObj = entry.getValue().getAsJsonObject(); + String projectId = versionObj.get("project_id").getAsString(); + String versionType = versionObj.get("version_type").getAsString(); + String versionNumber = versionObj.get("version_number").getAsString(); + String versionId = versionObj.get("id").getAsString(); + List files = new ArrayList<>(); + versionObj.get("files").getAsJsonArray().forEach(files::add); + Optional primaryFile = files.stream() + .filter(file -> file.getAsJsonObject().get("primary").getAsBoolean()).findFirst(); + + if (!primaryFile.isPresent()) { + return; + } + + Instant date; + + try { + date = Instant.parse(versionObj.get("date_published").getAsString()); + } catch (DateTimeParseException e) { + return; + } + + UpdateChannel updateChannel = UpdateCheckerUtil.getUpdateChannel(versionType); + String versionHash = primaryFile.get().getAsJsonObject().get("hashes").getAsJsonObject().get("sha512").getAsString(); + + results.put(lookupHash, new VersionUpdate(projectId, versionId, versionNumber, date, updateChannel, versionHash)); + }); + + return results; + } + } catch (IOException e) { + LOGGER.error("Error checking for updates: ", e); + } + + return null; + } + + private static class VersionUpdate { + String projectId; + String versionId; + String versionNumber; + Instant releaseDate; + UpdateChannel updateChannel; + String hash; + + public VersionUpdate(String projectId, String versionId, String versionNumber, Instant releaseDate, UpdateChannel updateChannel, String has) { + this.projectId = projectId; + this.versionId = versionId; + this.versionNumber = versionNumber; + this.releaseDate = releaseDate; + this.updateChannel = updateChannel; + this.hash = has; + } + + private UpdateInfo asUpdateInfo() { + return new ModrinthUpdateInfo(this.projectId, this.versionId, this.versionNumber, this.updateChannel); + } + } + + public static class LatestVersionsFromHashesBody { + public Collection hashes; + public String algorithm = "sha512"; + public Collection loaders; + @SerializedName("game_versions") + public Collection gameVersions; + @SerializedName("version_types") + public Collection versionTypes; + + public LatestVersionsFromHashesBody(Collection hashes, Collection loaders, String mcVersion, Collection updateChannels) { + this.hashes = hashes; + this.loaders = loaders; + this.gameVersions = new HashSet<>(); + this.gameVersions.add(mcVersion); + + this.versionTypes = new HashSet<>(); + + for (UpdateChannel updateChannel : updateChannels) { + this.versionTypes.add(updateChannel.toString().toLowerCase()); + } + } + } +} diff --git a/src/main/java/com/terraformersmc/modmenu/util/VersionUtil.java b/src/main/java/com/terraformersmc/modmenu/util/VersionUtil.java index 140267cf..deb0f067 100644 --- a/src/main/java/com/terraformersmc/modmenu/util/VersionUtil.java +++ b/src/main/java/com/terraformersmc/modmenu/util/VersionUtil.java @@ -6,9 +6,7 @@ public final class VersionUtil { private static final List PREFIXES = Arrays.asList("version", "ver", "v"); - private VersionUtil() { - return; - } + private VersionUtil() {} public static String stripPrefix(String version) { version = version.trim(); @@ -25,4 +23,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/Mod.java b/src/main/java/com/terraformersmc/modmenu/util/mod/Mod.java index b4764389..585c5476 100644 --- a/src/main/java/com/terraformersmc/modmenu/util/mod/Mod.java +++ b/src/main/java/com/terraformersmc/modmenu/util/mod/Mod.java @@ -1,5 +1,9 @@ package com.terraformersmc.modmenu.util.mod; +import com.google.common.base.Predicates; +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.TranslationUtil; import com.terraformersmc.modmenu.util.mod.fabric.FabricIconHandler; @@ -70,11 +74,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(); @@ -99,17 +109,31 @@ 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() && updateInfo.getUpdateChannel().compareTo(ModMenuConfig.UPDATE_CHANNEL.getValue()) >= 0; + } default @Nullable String getSha512Hash() throws IOException { return null; } - void setModrinthData(ModrinthData modrinthData); - void setChildHasUpdate(); boolean getChildHasUpdate(); @@ -148,8 +172,17 @@ public int getFillColor() { return this.fillColor; } - public static Set convert(Set badgeKeys) { - return badgeKeys.stream().map(KEY_MAP::get).collect(Collectors.toSet()); + public static Set convert(Set badgeKeys, String modId) { + return badgeKeys.stream() + .map(key -> { + if (!KEY_MAP.containsKey(key)) { + ModMenu.LOGGER.warn("Skipping unknown badge key '{}' specified by mod '{}'", key, modId); + } + + return KEY_MAP.get(key); + }) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); } static { 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 181f58e9..00000000 --- a/src/main/java/com/terraformersmc/modmenu/util/mod/ModrinthData.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.terraformersmc.modmenu.util.mod; - -import java.util.Objects; - -public class ModrinthData { - private final String projectId; - private final String versionId; - private final String versionNumber; - - public ModrinthData(String projectId, String versionId, String versionNumber) { - this.projectId = projectId; - this.versionId = versionId; - this.versionNumber = versionNumber; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof ModrinthData)) { - return false; - } - ModrinthData other = (ModrinthData)o; - return Objects.equals(projectId, other.projectId) && Objects.equals(versionId, other.versionId) && Objects.equals(versionNumber, other.versionNumber); - } - - @Override - public int hashCode() { - return Objects.hash(projectId, versionId, versionNumber); - } - - @Override - public String toString() { - return String.format("ModrinthData[projectId: %s, versionId: %s, versionNumber: %s]", projectId, versionId, versionNumber); - } - - public String projectId() { - return projectId; - } - - public String versionId() { - return versionId; - } - - public String versionNumber() { - return 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..ae542f32 --- /dev/null +++ b/src/main/java/com/terraformersmc/modmenu/util/mod/ModrinthUpdateInfo.java @@ -0,0 +1,58 @@ +package com.terraformersmc.modmenu.util.mod; + +import org.jetbrains.annotations.Nullable; + +import com.terraformersmc.modmenu.api.UpdateChannel; +import com.terraformersmc.modmenu.api.UpdateInfo; +import com.terraformersmc.modmenu.util.VersionUtil; + +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; + +public class ModrinthUpdateInfo implements UpdateInfo { + protected final String projectId; + protected final String versionId; + protected final String versionNumber; + protected final UpdateChannel updateChannel; + + private static final Text MODRINTH_TEXT = new TranslatableText("modmenu.modrinth"); + + public ModrinthUpdateInfo(String projectId, String versionId, String versionNumber, UpdateChannel updateChannel) { + this.projectId = projectId; + this.versionId = versionId; + this.versionNumber = versionNumber; + this.updateChannel = updateChannel; + } + + @Override + public boolean isUpdateAvailable() { + return true; + } + + @Override + public @Nullable Text getUpdateMessage() { + return new TranslatableText("modmenu.updateText", VersionUtil.stripPrefix(this.versionNumber), MODRINTH_TEXT); + } + + @Override + public String getDownloadLink() { + return String.format("https://modrinth.com/project/%s/version/%s", projectId, versionId); + } + + public String getProjectId() { + return projectId; + } + + public String getVersionId() { + return versionId; + } + + public String getVersionNumber() { + return versionNumber; + } + + @Override + public UpdateChannel getUpdateChannel() { + return this.updateChannel; + } +} 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 764cee6b..a3875c9b 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,11 @@ package com.terraformersmc.modmenu.util.mod.fabric; +import com.google.common.collect.ImmutableMap; 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.render.texture.DynamicTexture; @@ -88,13 +90,13 @@ public FabricDummyParentMod(FabricMod host, String id) { } @Override - public @NotNull List getContributors() { - return new ArrayList<>(); + public @NotNull Map> getContributors() { + return ImmutableMap.of(); } @Override - public @NotNull List getCredits() { - return new ArrayList<>(); + public @NotNull SortedMap> getCredits() { + return new TreeMap<>(); } @Override @@ -157,18 +159,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/FabricLoaderUpdateChecker.java b/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricLoaderUpdateChecker.java new file mode 100644 index 00000000..2fc377e1 --- /dev/null +++ b/src/main/java/com/terraformersmc/modmenu/util/mod/fabric/FabricLoaderUpdateChecker.java @@ -0,0 +1,162 @@ +package com.terraformersmc.modmenu.util.mod.fabric; + +import java.io.IOException; +import java.net.URI; +import java.util.Optional; + +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.util.EntityUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Nullable; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +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; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; + +public class FabricLoaderUpdateChecker implements UpdateChecker { + public static final Logger LOGGER = LogManager.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 { + UpdateChannel preferredChannel = UpdateChannel.getUserPreference(); + + RequestBuilder request = RequestBuilder.get().setUri(LOADER_VERSIONS); + HttpResponse response = HttpUtil.request(request); + + int status = response.getStatusLine().getStatusCode(); + + if (status != 200) { + LOGGER.warn("Fabric Meta responded with a non-200 status: {}!", status); + return null; + } + + Header[] contentType = response.getHeaders("Content-Type"); + + if (contentType.length == 0 || !contentType[0].getValue().contains("application/json")) { + LOGGER.warn("Fabric Meta responded with a non-json content type, aborting loader update check!"); + return null; + } + + JsonElement data = new JsonParser().parse(EntityUtils.toString(response.getEntity())); + + if (!data.isJsonArray()) { + LOGGER.warn("Received invalid data from Fabric Meta, aborting loader update check!"); + return null; + } + + SemanticVersion match = null; + boolean stableVersion = true; + + for (JsonElement child : data.getAsJsonArray()) { + if (!child.isJsonObject()) { + continue; + } + + JsonObject object = child.getAsJsonObject(); + Optional version = JsonUtil.getString(object, "version"); + + if (!version.isPresent()) { + 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 ... + boolean 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(match.getFriendlyString(), 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 String version; + private final boolean isStable; + + private FabricLoaderUpdateInfo(String version, boolean isStable) { + this.version = version; + this.isStable = isStable; + } + + @Override + public boolean isUpdateAvailable() { + return true; + } + + @Override + public @Nullable Text getUpdateMessage() { + return new TranslatableText("modmenu.install_version", this.version); + } + + @Override + public String getDownloadLink() { + return "https://ornithemc.net"; + } + + @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 4134ec60..d9ab0acc 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.*; @@ -39,7 +40,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; @@ -51,7 +53,9 @@ 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())) { + String id = metadata.getId(); + + if ("minecraft".equals(id) || "java".equals(id)) { allowsUpdateChecks = false; } @@ -77,13 +81,13 @@ public FabricMod(ModContainer modContainer, Set modpackMods) { CustomValueUtil.getString("icon", parentObj), CustomValueUtil.getStringSet("badges", parentObj).orElse(new HashSet<>()) ); - if (parentId.orElse("").equals(this.metadata.getId())) { + if (parentId.orElse("").equals(id)) { parentId = Optional.empty(); parentData = null; throw new RuntimeException("Mod declared itself as its own parent"); } } catch (Throwable t) { - LOGGER.error("Error loading parent data from mod: " + metadata.getId(), t); + LOGGER.error("Error loading parent data from mod: " + id, t); } } } @@ -94,12 +98,12 @@ public FabricMod(ModContainer modContainer, Set modpackMods) { this.modMenuData = new ModMenuData( badgeNames, parentId, - parentData + parentData, + id ); /* Hardcode parents and badges for OSL & Fabric Loader */ - String id = metadata.getId(); - if (id.startsWith("osl-")) { + if (metadata.getId().startsWith("osl-")) { modMenuData.fillParentIfEmpty("osl"); modMenuData.badges.add(Badge.LIBRARY); } @@ -207,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 LinkedHashMap<>(); + + for (Person contributor : this.metadata.getContributors()) { + contributors.put(contributor.getName(), Arrays.asList("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<>(); + + List authors = this.getAuthors(); + Map> contributors = this.getContributors(); + + for (String author : authors) { + contributors.put(author, Arrays.asList("Author")); + } + + for (Map.Entry> contributor : contributors.entrySet()) { + for (String role : contributor.getValue()) { + credits.computeIfAbsent(role, key -> new LinkedHashSet<>()); + credits.get(role).add(contributor.getKey()); + } + } + + return credits; } @Override @@ -275,20 +294,34 @@ public boolean isReal() { } @Override - public @Nullable ModrinthData getModrinthData() { - return this.modrinthData; + public boolean allowsUpdateChecks() { + if (ModMenuConfig.DISABLE_UPDATE_CHECKER.getValue().contains(this.getId())) { + return false; + } + + return this.allowsUpdateChecks; } @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(); } } @@ -332,8 +365,8 @@ static class ModMenuData { private @Nullable final DummyParentData dummyParentData; - public ModMenuData(Set badges, Optional parent, DummyParentData dummyParentData) { - this.badges = Badge.convert(badges); + public ModMenuData(Set badges, Optional parent, DummyParentData dummyParentData, String id) { + this.badges = Badge.convert(badges, id); this.parent = parent; this.dummyParentData = dummyParentData; } @@ -380,7 +413,7 @@ public DummyParentData(String id, Optional name, Optional descri this.name = name; this.description = description; this.icon = icon; - this.badges = Badge.convert(badges); + this.badges = Badge.convert(badges, id); } public String getId() { 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..8225f289 --- /dev/null +++ b/src/main/java/com/terraformersmc/modmenu/util/mod/quilt/QuiltLoaderUpdateChecker.java @@ -0,0 +1,167 @@ +package com.terraformersmc.modmenu.util.mod.quilt; + +import java.io.IOException; +import java.net.URI; +import java.util.Optional; + +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.util.EntityUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Nullable; +import org.quiltmc.loader.api.QuiltLoader; +import org.quiltmc.loader.api.Version; +import org.quiltmc.loader.api.VersionFormatException; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +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 net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; + +public class QuiltLoaderUpdateChecker implements UpdateChecker { + public static final Logger LOGGER = LogManager.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 { + UpdateChannel preferredChannel = UpdateChannel.getUserPreference(); + + RequestBuilder request = RequestBuilder.get().setUri(LOADER_VERSIONS); + HttpResponse response = HttpUtil.request(request); + + int status = response.getStatusLine().getStatusCode(); + + if (status != 200) { + LOGGER.warn("Quilt Meta responded with a non-200 status: {}!", status); + return null; + } + + Header[] contentType = response.getHeaders("Content-Type"); + + if (contentType.length == 0 || !contentType[0].getValue().contains("application/json")) { + LOGGER.warn("Quilt Meta responded with a non-json content type, aborting loader update check!"); + return null; + } + + JsonElement data = new JsonParser().parse(EntityUtils.toString(response.getEntity())); + + if (!data.isJsonArray()) { + LOGGER.warn("Received invalid data from Quilt Meta, aborting loader update check!"); + return null; + } + + Version.Semantic match = null; + + for (JsonElement child : data.getAsJsonArray()) { + if (!child.isJsonObject()) { + continue; + } + + JsonObject object = child.getAsJsonObject(); + Optional version = JsonUtil.getString(object, "version"); + + if (!version.isPresent()) { + 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!"); + return new QuiltLoaderUpdateInfo(match); + } + + 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 Version.Semantic version; + + private QuiltLoaderUpdateInfo(Version.Semantic version) { + this.version = version; + } + + @Override + public boolean isUpdateAvailable() { + return true; + } + + @Override + public @Nullable Text getUpdateMessage() { + return new TranslatableText("modmenu.install_version", this.version.raw()); + } + + @Override + public String getDownloadLink() { + return "https://ornithemc.net"; + } + + @Override + public UpdateChannel getUpdateChannel() { + String 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/java/com/terraformersmc/modmenu/util/mod/quilt/QuiltMod.java b/src/main/java/com/terraformersmc/modmenu/util/mod/quilt/QuiltMod.java index 8d335fc1..9fd415fc 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 @@ -3,7 +3,7 @@ import com.google.common.collect.Lists; import com.google.common.hash.Hashing; import com.google.common.io.Files; -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; @@ -16,9 +16,16 @@ import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Path; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; import java.util.stream.Collectors; public class QuiltMod extends FabricMod { @@ -52,24 +59,37 @@ 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 LinkedHashMap<>(); + + for (ModContributor 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<>(); + + Map> contributors = this.getContributors(); + + for (Map.Entry> contributor : contributors.entrySet()) { + for (String role : contributor.getValue()) { + credits.computeIfAbsent(role, key -> new LinkedHashSet<>()); + credits.get(role).add(contributor.getKey()); + } + } + + return credits; } public @Nullable String getSha512Hash() throws IOException { String fabricResult = super.getSha512Hash(); if (fabricResult == null) { - ModrinthUtil.LOGGER.debug("Checking {}", getId()); + UpdateCheckerUtil.LOGGER.debug("Checking {}", getId()); if (container.getSourceType().equals(ModContainer.BasicSourceType.NORMAL_QUILT) || container.getSourceType().equals(ModContainer.BasicSourceType.NORMAL_FABRIC)) { for (List paths : container.getSourcePaths()) { List jars = paths.stream().filter(p -> p.toString().toLowerCase(Locale.ROOT).endsWith(".jar")).collect(Collectors.toList()); @@ -78,7 +98,7 @@ public QuiltMod(net.fabricmc.loader.api.ModContainer fabricModContainer, Set