diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModListPage.java index cc11d1d7125..6d0e63f24e8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModListPage.java @@ -38,6 +38,7 @@ import org.jackhuang.hmcl.ui.construct.PageAware; import org.jackhuang.hmcl.util.TaskCancellationAction; import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jetbrains.annotations.Nullable; import java.io.IOException; @@ -57,7 +58,7 @@ public final class ModListPage extends ListPageBase supportedLoaders = EnumSet.noneOf(ModLoaderType.class); @@ -137,18 +138,27 @@ private void updateSupportedLoaders(ModManager modManager) { return; } + var gameVersionNumber = GameVersionNumber.asGameVersion(gameVersion); + for (LibraryAnalyzer.LibraryType type : LibraryAnalyzer.LibraryType.values()) { if (type.isModLoader() && analyzer.has(type)) { ModLoaderType modLoaderType = type.getModLoaderType(); if (modLoaderType != null) { supportedLoaders.add(modLoaderType); - - if (modLoaderType == ModLoaderType.CLEANROOM) - supportedLoaders.add(ModLoaderType.FORGE); } } } + if (analyzer.has(LibraryAnalyzer.LibraryType.CLEANROOM)) { + supportedLoaders.add(ModLoaderType.FORGE); + } + + if (analyzer.has(LibraryAnalyzer.LibraryType.FORGE) // No cleanroom because LiteLoader cannot run on Java 21 + && modManager.hasLiteLoaderAsMod() + && !gameVersionNumber.isAtLeast("1.13", "17w43a")) { + supportedLoaders.add(ModLoaderType.LITE_LOADER); + } + if (analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && "1.20.1".equals(gameVersion)) { supportedLoaders.add(ModLoaderType.FORGE); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModListPageSkin.java index 154705397d1..ea5733ed48b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModListPageSkin.java @@ -63,6 +63,7 @@ import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.NetworkUtils; +import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jetbrains.annotations.NotNullByDefault; import org.jetbrains.annotations.Nullable; @@ -551,9 +552,9 @@ final class ModInfoListCell extends MDListCell { JFXButton restoreButton = FXUtils.newToggleButton4(SVG.RESTORE); JFXButton infoButton = FXUtils.newToggleButton4(SVG.INFO); JFXButton revealButton = FXUtils.newToggleButton4(SVG.FOLDER); - BooleanProperty booleanProperty; + @Nullable BooleanProperty booleanProperty; - Tooltip warningTooltip; + @Nullable Tooltip warningTooltip; ModInfoListCell(JFXListView listView) { super(listView); @@ -628,18 +629,41 @@ protected void updateControl(ModInfoObject dataItem, boolean empty) { content.setSubtitle(joiner.toString()); - if (modLoaderType == ModLoaderType.UNKNOWN) { - content.addTagWarning(i18n("mods.unknown")); - } else if (!ModListPageSkin.this.getSkinnable().supportedLoaders.contains(modLoaderType)) { - warning.add(i18n("mods.warning.loader_mismatch")); - switch (dataItem.getModInfo().getModLoaderType()) { - case FORGE -> content.addTagWarning(i18n("install.installer.forge")); - case LEGACY_FABRIC -> content.addTagWarning(i18n("install.installer.legacyfabric")); - case CLEANROOM -> content.addTagWarning(i18n("install.installer.cleanroom")); - case NEO_FORGE -> content.addTagWarning(i18n("install.installer.neoforge")); - case FABRIC -> content.addTagWarning(i18n("install.installer.fabric")); - case LITE_LOADER -> content.addTagWarning(i18n("install.installer.liteloader")); - case QUILT -> content.addTagWarning(i18n("install.installer.quilt")); + { + Set modFileLoaders = EnumSet.noneOf(ModLoaderType.class); + GameVersionNumber gameVersionNumber = GameVersionNumber.asGameVersion(Optional.ofNullable(getSkinnable().gameVersion)); + // Uses 1.7 snapshot as there's no snapshots for 1.6 after its first release + boolean checkCoreModsDir = !gameVersionNumber.isAtLeast("1.6.1", "13w36a"); + + if (modLoaderType != ModLoaderType.UNKNOWN) modFileLoaders.add(modLoaderType); + modFileLoaders.addAll(modInfo.getCoreMods().getModLoaders(gameVersionNumber)); + + if (modFileLoaders.stream().anyMatch(loader -> getSkinnable().supportedLoaders.contains(loader))) { + if (modInfo.isCoreMod()) { + if (checkCoreModsDir) { + content.addTagWarning("CoreMod"); + warning.add(i18n("mods.coremods.check_dir")); + } else { + content.addTag("CoreMod"); + } + } + } else { + if (modLoaderType == ModLoaderType.UNKNOWN) { + content.addTagWarning(i18n("mods.unknown")); + } else { + warning.add(i18n("mods.warning.loader_mismatch")); + switch (modLoaderType) { + case FORGE -> content.addTagWarning(i18n("install.installer.forge")); + case LEGACY_FABRIC -> content.addTagWarning(i18n("install.installer.legacyfabric")); + case CLEANROOM -> content.addTagWarning(i18n("install.installer.cleanroom")); + case NEO_FORGE -> content.addTagWarning(i18n("install.installer.neoforge")); + case FABRIC -> content.addTagWarning(i18n("install.installer.fabric")); + case LITE_LOADER -> content.addTagWarning(i18n("install.installer.liteloader")); + case QUILT -> content.addTagWarning(i18n("install.installer.quilt")); + } + } + if (modInfo.isCoreMod()) + content.addTagWarning("CoreMod"); } } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index ca60b1b0a01..5edb03d63d3 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1202,6 +1202,7 @@ mods.add=Add mods.add.failed=Failed to add mod %s. mods.add.success=%s was successfully added. mods.add.title=Choose mod file you want to add +mods.coremods.check_dir=Should put into 'coremods' directory mods.disable=Disable mods.download=Download Mod mods.download.title=Download Mod - %1s diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 5eeafafcb3b..c8edf84f65f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1009,6 +1009,7 @@ mods.add=新增模組 mods.add.failed=新增模組「%s」失敗。 mods.add.success=成功新增模組「%s」。 mods.add.title=選取要新增的模組檔案 +mods.coremods.check_dir=應放入 "coremods" 資料夾 mods.disable=停用 mods.download=下載模組 mods.download.title=模組下載 - %1s diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 6fe08134e54..a8db7f178f9 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1009,6 +1009,7 @@ mods.add=添加模组 mods.add.failed=添加模组“%s”失败。\n如遇到问题,你可以点击右上角帮助按钮进行求助。 mods.add.success=成功添加模组 %s。 mods.add.title=选择要添加的模组文件 +mods.coremods.check_dir=应放入 "coremods" 文件夹 mods.disable=禁用 mods.download=下载模组 mods.download.title=模组下载 - %1s diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/CoreMods.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/CoreMods.java new file mode 100644 index 00000000000..ee08e44cc4e --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/CoreMods.java @@ -0,0 +1,173 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.addon.meta; + +import com.google.gson.JsonIOException; +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; +import kala.compress.archivers.zip.ZipArchiveEntry; +import org.jackhuang.hmcl.addon.mod.ModLoaderType; +import org.jackhuang.hmcl.util.Immutable; +import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.tree.ZipFileTree; +import org.jackhuang.hmcl.util.versioning.GameVersionNumber; +import org.jackhuang.hmcl.util.versioning.VersionRange; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Path; +import java.util.*; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +/// @see CoreModTutor by xfl03 and contributors +@Immutable +public final class CoreMods { + + public static final CoreMods EMPTY = new CoreMods(); + + private final Map>> coreMods; + private final boolean liteLoaderAsMod; + + private CoreMods() { + this.coreMods = Collections.emptyMap(); + this.liteLoaderAsMod = false; + } + + private CoreMods(List coreModList, boolean liteloaderAsMod) { + EnumMap>> map = new EnumMap<>(ModLoaderType.class); + for (var coreMod : coreModList) { + map.computeIfAbsent(coreMod.modLoaderType(), k -> new ArrayList<>()).add(coreMod.mcVersionRange()); + } + this.coreMods = Collections.unmodifiableMap(map); + this.liteLoaderAsMod = liteloaderAsMod; + } + + public boolean isEmpty() { + return coreMods.isEmpty(); + } + + public boolean isLiteLoaderAsMod() { + return liteLoaderAsMod; + } + + public Set getModLoaders(GameVersionNumber gameVersionNumber) { + EnumSet supportedLoaders = EnumSet.noneOf(ModLoaderType.class); + if (gameVersionNumber == GameVersionNumber.unknown()) return supportedLoaders; + for (var entry : coreMods.entrySet()) + if (entry.getValue().stream().anyMatch(r -> r.contains(gameVersionNumber))) + supportedLoaders.add(entry.getKey()); + return supportedLoaders; + } + + @NotNull + public static CoreMods fromFile(Path modFile, ZipFileTree tree) { + List coreModList = new ArrayList<>(); + boolean liteloaderAsMod = false; + { + // Below 1.13 + ZipArchiveEntry mf = tree.getEntry("META-INF/MANIFEST.MF"); + if (mf != null) { + Attributes attr = null; + try (var in = tree.getInputStream(mf)) { + attr = new Manifest(in).getMainAttributes(); + } catch (IOException e) { + LOG.warning("Failed to load jar manifest for jar: " + modFile, e); + } + if (attr != null) { + String fmlCorePlg = attr.getValue("FMLCorePlugin"); + String tweakClass = attr.getValue("TweakClass"); + if (StringUtils.isNotBlank(fmlCorePlg)) + coreModList.addAll(List.of( + new CoreMod(ModLoaderType.FORGE, "1.3.2", "1.12.2"), + new CoreMod(ModLoaderType.CLEANROOM, "1.12.2", "1.12.2") // TODO further testing + )); + if (StringUtils.isNotBlank(tweakClass)) { + if (tweakClass.trim().startsWith("com.mumfrey.liteloader")) liteloaderAsMod = true; + coreModList.addAll(List.of( + new CoreMod(ModLoaderType.FORGE, "1.6.1", "1.12.2"), + new CoreMod(ModLoaderType.LITE_LOADER, "1.6.1", "1.12.2"), + new CoreMod(ModLoaderType.CLEANROOM, "1.12.2", "1.12.2") + )); + } + } + } + } + { + // coremods.json in Forge 1.13-1.21.10 & NeoForge 1.21.4- + // TODO Find a sample + ZipArchiveEntry coreModsJson = tree.getEntry("META-INF/coremods.json"); + if (coreModsJson != null) { + try (var in = new InputStreamReader(tree.getInputStream(coreModsJson))) { + var map = JsonUtils.fromJson(in, new TypeToken>() { + }); + if (map != null && !map.isEmpty()) + coreModList.addAll(List.of( + // Removed in https://github.com/MinecraftForge/MinecraftForge/pull/10746 + new CoreMod(ModLoaderType.FORGE, "1.13", "1.21.10"), + // Removed in https://github.com/neoforged/NeoForge/pull/2072 + new CoreMod(ModLoaderType.NEO_FORGE, "1.20.1", "1.21.4") + )); + } catch (IOException | JsonIOException e) { + LOG.warning("Failed to read coremods.json for jar: " + modFile, e); + } catch (JsonSyntaxException ignored) { + } + } + } + { + // ITransformationService + if (tree.getEntry("META-INF/services/cpw.mods.modlauncher.api.ITransformationService") != null) + coreModList.addAll(List.of( + new CoreMod(ModLoaderType.FORGE, "1.13.2"), + new CoreMod(ModLoaderType.NEO_FORGE, "1.20.1", "1.21.8") // Replaced by ClassProcessorProvider + )); + } + { + // ICoreMod for NeoForge + // https://github.com/neoforged/FancyModLoader/pull/79 + // https://neoforged.net/news/2024-retrospection/#the-changes + // TODO Find a sample + if (tree.getEntry("META-INF/services/net.neoforged.neoforgespi.coremod.ICoreMod") != null) + coreModList.add(new CoreMod(ModLoaderType.NEO_FORGE, "1.20.5", "1.21.8")); // Replaced by ClassProcessorProvider + } + { + // ClassProcessorProvider for NeoForge, replaces ITransformationService & ICoreMod + // https://github.com/neoforged/NeoForge/pull/2655 + // https://github.com/neoforged/FancyModLoader/pull/358 + if (tree.getEntry("META-INF/services/net.neoforged.neoforgespi.transformation.ClassProcessorProvider") != null) + coreModList.add(new CoreMod(ModLoaderType.NEO_FORGE, "1.21.9")); + } + return new CoreMods(coreModList, liteloaderAsMod); + } + + private record CoreMod(ModLoaderType modLoaderType, VersionRange mcVersionRange) { + + private CoreMod(ModLoaderType modLoaderType, String min) { + this(modLoaderType, GameVersionNumber.atLeast(min)); + } + + private CoreMod(ModLoaderType modLoaderType, String min, String max) { + this(modLoaderType, GameVersionNumber.between(min, max)); + } + + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/FabricModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/FabricModMetadata.java index 4601ed1ca5d..6ac8cd854a8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/FabricModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/FabricModMetadata.java @@ -60,14 +60,14 @@ public FabricModMetadata(String id, String name, String version, String icon, St this.contact = contact; } - public static LocalModFile fromFile(ModManager modManager, Path modFile, ZipFileTree tree) throws IOException, JsonParseException { + public static LocalModFile fromFile(ModManager modManager, Path modFile, ZipFileTree tree, CoreMods coreMods) throws IOException, JsonParseException { ZipArchiveEntry mcmod = tree.getEntry("fabric.mod.json"); if (mcmod == null) throw new IOException("File " + modFile + " is not a Fabric mod."); FabricModMetadata metadata = JsonUtils.fromNonNullJsonFully(tree.getInputStream(mcmod), FabricModMetadata.class); String authors = metadata.authors == null ? "" : metadata.authors.stream().map(author -> author.name).collect(Collectors.joining(", ")); return new LocalModFile(modManager, modManager.getLocalMod(metadata.id, ModLoaderType.FABRIC), modFile, metadata.name, new LocalAddonFile.Description(metadata.description), - authors, metadata.version, "", metadata.contact != null ? metadata.contact.getOrDefault("homepage", "") : "", metadata.icon); + authors, metadata.version, "", metadata.contact != null ? metadata.contact.getOrDefault("homepage", "") : "", metadata.icon, coreMods); } @JsonAdapter(FabricModAuthorSerializer.class) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeNewModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeNewModMetadata.java index 6b7ff0085b4..19d79d2032a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeNewModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeNewModMetadata.java @@ -36,10 +36,7 @@ import org.jackhuang.hmcl.util.gson.Validation; import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.tree.ZipFileTree; -import org.tomlj.Toml; -import org.tomlj.TomlArray; -import org.tomlj.TomlParseResult; -import org.tomlj.TomlTable; +import org.tomlj.*; import java.io.IOException; import java.io.InputStream; @@ -177,33 +174,33 @@ public String deserialize(JsonElement authors, Type type, JsonDeserializationCon } } - public static LocalModFile fromForgeFile(ModManager modManager, Path modFile, ZipFileTree tree) throws IOException { - return fromFile(modManager, modFile, tree, ModLoaderType.FORGE); + public static LocalModFile fromForgeFile(ModManager modManager, Path modFile, ZipFileTree tree, CoreMods coreMods) throws IOException { + return fromFile(modManager, modFile, tree, ModLoaderType.FORGE, coreMods); } - public static LocalModFile fromNeoForgeFile(ModManager modManager, Path modFile, ZipFileTree tree) throws IOException { - return fromFile(modManager, modFile, tree, ModLoaderType.NEO_FORGE); + public static LocalModFile fromNeoForgeFile(ModManager modManager, Path modFile, ZipFileTree tree, CoreMods coreMods) throws IOException { + return fromFile(modManager, modFile, tree, ModLoaderType.NEO_FORGE, coreMods); } - private static LocalModFile fromFile(ModManager modManager, Path modFile, ZipFileTree tree, ModLoaderType modLoaderType) throws IOException { + private static LocalModFile fromFile(ModManager modManager, Path modFile, ZipFileTree tree, ModLoaderType modLoaderType, CoreMods coreMods) throws IOException { if (modLoaderType != ModLoaderType.FORGE && modLoaderType != ModLoaderType.NEO_FORGE) { throw new IOException("Invalid mod loader: " + modLoaderType); } if (modLoaderType == ModLoaderType.NEO_FORGE) { try { - return fromFile0("META-INF/neoforge.mods.toml", modLoaderType, modManager, modFile, tree); + return fromFile0("META-INF/neoforge.mods.toml", modLoaderType, modManager, modFile, tree, coreMods); } catch (Exception ignored) { } } try { - return fromFile0("META-INF/mods.toml", modLoaderType, modManager, modFile, tree); + return fromFile0("META-INF/mods.toml", modLoaderType, modManager, modFile, tree, coreMods); } catch (Exception ignored) { } try { - return fromEmbeddedMod(modManager, modFile, tree, modLoaderType); + return fromEmbeddedMod(modManager, modFile, tree, modLoaderType, coreMods); } catch (Exception ignored) { } @@ -215,7 +212,8 @@ private static LocalModFile fromFile0( ModLoaderType modLoaderType, ModManager modManager, Path modFile, - ZipFileTree tree) throws IOException, JsonParseException { + ZipFileTree tree, + CoreMods coreMods) throws IOException, JsonParseException { ZipArchiveEntry modToml = tree.getEntry(tomlPath); if (modToml == null) throw new IOException("File " + modFile + " is not a Forge 1.13+ or NeoForge mod."); @@ -247,10 +245,10 @@ private static LocalModFile fromFile0( return new LocalModFile(modManager, modManager.getLocalMod(mod.getModId(), type), modFile, mod.getDisplayName(), new LocalAddonFile.Description(mod.getDescription()), mod.getAuthors(), jarVersion == null ? mod.getVersion() : mod.getVersion().replace("${file.jarVersion}", jarVersion), "", mod.getDisplayURL(), - logoPath); + logoPath, coreMods); } - private static LocalModFile fromEmbeddedMod(ModManager modManager, Path modFile, ZipFileTree tree, ModLoaderType modLoaderType) throws IOException { + private static LocalModFile fromEmbeddedMod(ModManager modManager, Path modFile, ZipFileTree tree, ModLoaderType modLoaderType, CoreMods coreMods) throws IOException { ZipArchiveEntry manifestFile = tree.getEntry("META-INF/MANIFEST.MF"); if (manifestFile == null) throw new IOException("Missing MANIFEST.MF in file " + modFile); @@ -300,7 +298,7 @@ private static LocalModFile fromEmbeddedMod(ModManager modManager, Path modFile, for (ZipArchiveEntry embeddedModFile : embeddedModFiles) { tree.extractTo(embeddedModFile, tempFile); try (ZipFileTree embeddedTree = CompressingUtils.openZipTree(tempFile)) { - return fromFile(modManager, modFile, embeddedTree, modLoaderType); + return fromFile(modManager, modFile, embeddedTree, modLoaderType, coreMods); } catch (Exception ignored) { } } @@ -318,7 +316,7 @@ private static ModLoaderType analyzeLoader(TomlParseResult toml, String modID, M if (tomlArray != null) { dependencies = tomlArray.toList().stream().map( o -> ((TomlTable) o).toMap()).toList(); } - } catch (ClassCastException ignored) { // https://github.com/HMCL-dev/HMCL/issues/5068 + } catch (ClassCastException | TomlInvalidTypeException ignored) { // https://github.com/HMCL-dev/HMCL/issues/5068 } if (dependencies == null) { @@ -327,7 +325,7 @@ private static ModLoaderType analyzeLoader(TomlParseResult toml, String modID, M if (tomlArray != null) { dependencies = tomlArray.toList().stream().map( o -> ((TomlTable) o).toMap()).toList(); } - } catch (ClassCastException e) { + } catch (ClassCastException | TomlInvalidTypeException e) { try { TomlTable table = toml.getTable("dependencies"); if (table == null) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeOldModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeOldModMetadata.java index 81f358c94e6..56d88d43f24 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeOldModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeOldModMetadata.java @@ -124,7 +124,7 @@ public String[] getAuthors() { return authors; } - public static LocalModFile fromFile(ModManager modManager, Path modFile, ZipFileTree tree) throws IOException, JsonParseException { + public static LocalModFile fromFile(ModManager modManager, Path modFile, ZipFileTree tree, CoreMods coreMods) throws IOException, JsonParseException { ZipArchiveEntry mcmod = tree.getEntry("mcmod.info"); if (mcmod == null) throw new IOException("File " + modFile + " is not a Forge mod."); @@ -160,6 +160,6 @@ else if (firstToken == JsonToken.BEGIN_OBJECT) { return new LocalModFile(modManager, modManager.getLocalMod(metadata.getModId(), ModLoaderType.FORGE), modFile, metadata.getName(), new LocalAddonFile.Description(metadata.getDescription()), authors, metadata.getVersion(), metadata.getGameVersion(), StringUtils.isBlank(metadata.getUrl()) ? metadata.getUpdateUrl() : metadata.url, - metadata.getLogoFile()); + metadata.getLogoFile(), coreMods); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/LiteModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/LiteModMetadata.java index aea8c4fab2e..6022564a859 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/LiteModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/LiteModMetadata.java @@ -110,7 +110,7 @@ public String getUpdateURI() { return updateURI; } - public static LocalModFile fromFile(ModManager modManager, Path modFile, ZipFileTree tree) throws IOException, JsonParseException { + public static LocalModFile fromFile(ModManager modManager, Path modFile, ZipFileTree tree, CoreMods coreMods) throws IOException, JsonParseException { ZipArchiveEntry entry = tree.getEntry("litemod.json"); if (entry == null) throw new IOException("File " + modFile + " is not a LiteLoader mod."); @@ -118,7 +118,7 @@ public static LocalModFile fromFile(ModManager modManager, Path modFile, ZipFile if (metadata == null) throw new IOException("Mod " + modFile + " `litemod.json` is malformed."); return new LocalModFile(modManager, modManager.getLocalMod(metadata.getName(), ModLoaderType.LITE_LOADER), modFile, metadata.getName(), new LocalAddonFile.Description(metadata.getDescription()), metadata.getAuthor(), - metadata.getVersion(), metadata.getGameVersion(), metadata.getUpdateURI(), ""); + metadata.getVersion(), metadata.getGameVersion(), metadata.getUpdateURI(), "", coreMods); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/QuiltModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/QuiltModMetadata.java index 02e31ed7269..5f81e071e9a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/QuiltModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/QuiltModMetadata.java @@ -71,7 +71,7 @@ public QuiltModMetadata(int schemaVersion, QuiltLoader quiltLoader) { this.quilt_loader = quiltLoader; } - public static LocalModFile fromFile(ModManager modManager, Path modFile, ZipFileTree tree) throws IOException, JsonParseException { + public static LocalModFile fromFile(ModManager modManager, Path modFile, ZipFileTree tree, CoreMods coreMods) throws IOException, JsonParseException { ZipArchiveEntry path = tree.getEntry("quilt.mod.json"); if (path == null) { throw new IOException("File " + modFile + " is not a Quilt mod."); @@ -92,7 +92,7 @@ public static LocalModFile fromFile(ModManager modManager, Path modFile, ZipFile root.quilt_loader.version, "", Optional.ofNullable(root.quilt_loader.metadata.contact.get("homepage")).map(jsonElement -> jsonElement.getAsJsonPrimitive().getAsString()).orElse(""), - root.quilt_loader.metadata.icon + root.quilt_loader.metadata.icon, coreMods ); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/LocalModFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/LocalModFile.java index 32984fa3baa..15a5a53fa2a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/LocalModFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/LocalModFile.java @@ -23,8 +23,10 @@ import org.jackhuang.hmcl.addon.LocalAddonManager; import org.jackhuang.hmcl.addon.RemoteAddon; import org.jackhuang.hmcl.addon.RemoteAddonRepository; +import org.jackhuang.hmcl.addon.meta.CoreMods; import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.util.io.FileUtils; +import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.nio.file.Files; @@ -50,13 +52,15 @@ public final class LocalModFile extends LocalAddonFile implements Comparable { @FunctionalInterface private interface ModMetadataReader { - LocalModFile fromFile(ModManager modManager, Path modFile, ZipFileTree tree) throws IOException, JsonParseException; + LocalModFile fromFile(ModManager modManager, Path modFile, ZipFileTree tree, CoreMods coreMods) throws IOException, JsonParseException; } private static final Map>> READERS; @@ -66,6 +66,7 @@ private interface ModMetadataReader { } private final HashMap, LocalMod> localMods = new HashMap<>(); + private boolean hasLiteLoaderAsMod = false; private LibraryAnalyzer analyzer; private boolean loaded = false; @@ -102,6 +103,15 @@ public boolean hasMod(String modId, ModLoaderType modLoaderType) { } } + public boolean hasLiteLoaderAsMod() { + lock.lock(); + try { + return hasLiteLoaderAsMod; + } finally { + lock.unlock(); + } + } + private void addModInfo(Path file) { String fileName = StringUtils.removeSuffix(FileUtils.getName(file), DISABLED_EXTENSION, OLD_EXTENSION); String extension = fileName.substring(fileName.lastIndexOf(".") + 1); @@ -126,12 +136,15 @@ private void addModInfo(Path file) { } LocalModFile modInfo = null; + CoreMods coreMods = CoreMods.EMPTY; List exceptions = new ArrayList<>(); try (ZipFileTree tree = CompressingUtils.openZipTree(file)) { + coreMods = CoreMods.fromFile(file, tree); + for (ModMetadataReader reader : supportedReaders) { try { - modInfo = reader.fromFile(this, file, tree); + modInfo = reader.fromFile(this, file, tree, coreMods); break; } catch (Exception e) { exceptions.add(e); @@ -141,7 +154,7 @@ private void addModInfo(Path file) { if (modInfo == null) { for (ModMetadataReader reader : unsupportedReaders) { try { - modInfo = reader.fromFile(this, file, tree); + modInfo = reader.fromFile(this, file, tree, coreMods); break; } catch (Exception ignored) { } @@ -164,11 +177,13 @@ private void addModInfo(Path file) { getLocalMod(fileNameWithoutExtension, ModLoaderType.UNKNOWN), file, fileNameWithoutExtension, - new LocalAddonFile.Description("litemod".equals(extension) ? "LiteLoader Mod" : "") + new LocalAddonFile.Description("litemod".equals(extension) ? "LiteLoader Mod" : ""), + coreMods ); } if (!modInfo.isOld()) { + hasLiteLoaderAsMod |= coreMods.isLiteLoaderAsMod(); localFiles.add(modInfo); } } @@ -179,6 +194,7 @@ public void refresh() throws IOException { try { localFiles.clear(); localMods.clear(); + hasLiteLoaderAsMod = false; try { analyzer = LibraryAnalyzer.analyze(getRepository().getResolvedInstanceManifest(instanceId), null); @@ -325,34 +341,6 @@ public static boolean isFileNameMod(Path file) { return MOD_EXTENSIONS.contains(FileUtils.getExtension(name).toLowerCase(Locale.ROOT)); } - public static boolean isFileMod(Path modFile) { - try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) { - if (Files.exists(fs.getPath("mcmod.info")) || Files.exists(fs.getPath("META-INF/mods.toml"))) { - // Forge mod - return true; - } - - if (Files.exists(fs.getPath("fabric.mod.json"))) { - // Fabric mod - return true; - } - - if (Files.exists(fs.getPath("quilt.mod.json"))) { - // Quilt mod - return true; - } - - if (Files.exists(fs.getPath("litemod.json"))) { - // Liteloader mod - return true; - } - - return false; - } catch (IOException e) { - return false; - } - } - /** * Check if "mods" directory has mod file named "fileName" no matter the mod is disabled, upgraded or not * diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/GameVersionNumber.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/GameVersionNumber.java index 461732f0b06..ed3cc5b73f0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/GameVersionNumber.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/GameVersionNumber.java @@ -40,6 +40,7 @@ public static String[] getDefaultGameVersions() { return Versions.DEFAULT_GAME_VERSIONS; } + @NotNull public static GameVersionNumber asGameVersion(String version) { GameVersionNumber versionNumber = Versions.SPECIALS.get(version); if (versionNumber != null) @@ -71,8 +72,9 @@ public static GameVersionNumber asGameVersion(String version) { return new Special(version, version); } + @NotNull public static GameVersionNumber asGameVersion(Optional version) { - return version.isPresent() ? asGameVersion(version.get()) : unknown(); + return version.map(GameVersionNumber::asGameVersion).orElseGet(GameVersionNumber::unknown); } public static GameVersionNumber unknown() {