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 02d69ed8f00..abacc5dcac6 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 @@ -95,7 +95,7 @@ public final class ModListPage extends ListPageBase i private ModManager modManager; private @Nullable HMCLGameInstance gameInstance; - private String gameVersion; + String gameVersion; final EnumSet supportedLoaders = EnumSet.noneOf(ModLoaderType.class); @@ -185,18 +185,29 @@ private void updateSupportedLoaders(ModManager modManager) { return; } + var gameVersionNumber = GameVersionNumber.asGameVersion(gameVersion); + for (GameComponentType type : GameComponentType.MOD_LOADERS) { 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(GameComponentType.CLEANROOM)) { + supportedLoaders.add(ModLoaderType.FORGE); + } + + if (analyzer.has(GameComponentType.FORGE) // No cleanroom because LiteLoader cannot run on Java 21 + && modManager.hasLiteLoaderAsMod() + && !gameVersionNumber.isAtLeast("1.13", "17w43a") + // LiteLoader indicates that it supports 1.5.2 as well in this way, but it actually does nothing + && gameVersionNumber.isAtLeast("1.6.1", "13w36a" /* 1.7-snapshot-1 */)) { + supportedLoaders.add(ModLoaderType.LITE_LOADER); + } + if (analyzer.has(GameComponentType.NEO_FORGE) && "1.20.1".equals(gameVersion)) { supportedLoaders.add(ModLoaderType.FORGE); } @@ -888,18 +899,47 @@ protected void updateControl(ModInfoObject dataItem, boolean empty) { content.setSubtitle(joiner.toString()); - if (modLoaderType == ModLoaderType.UNKNOWN) { - content.addTagWarning(i18n("mods.unknown")); - } else if (!page.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(page.gameVersion)); + // Uses 1.7 snapshot as there's no snapshots for 1.6 after its first release + boolean wrongCoreModDir = !gameVersionNumber.isAtLeast("1.6.1", "13w36a") + && modInfo.getCoreModInfo().isLegacy() + && !"coremods".equals(modInfo.getSubfolderName()); + + if (modLoaderType != ModLoaderType.UNKNOWN) modFileLoaders.add(modLoaderType); + modFileLoaders.addAll(modInfo.getCoreModInfo().getModLoaders(gameVersionNumber)); + + if (modFileLoaders.stream().anyMatch(page.supportedLoaders::contains)) { + if (modInfo.isCoreMod()) { + if (wrongCoreModDir) { + content.addTagWarning("CoreMod"); + warning.add(i18n("mods.coremods.check_dir")); + } else { + content.addTag("CoreMod"); + } + } + } else { + if (modFileLoaders.isEmpty()) { + content.addTagWarning(i18n("mods.unknown")); + } else { + warning.add(i18n("mods.warning.loader_mismatch")); + for (var loaderType : modFileLoaders) { + switch (loaderType) { + 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"); + if (wrongCoreModDir) warning.add(i18n("mods.coremods.check_dir")); + } } } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 672b254bb31..9be6dac05ea 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1167,6 +1167,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 48e4524a3da..c298277d2b3 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -977,6 +977,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 cc1c47a2dc5..5d72dddd0eb 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -982,6 +982,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/CoreModInfo.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/CoreModInfo.java new file mode 100644 index 00000000000..9e6d468650a --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/CoreModInfo.java @@ -0,0 +1,179 @@ +/* + * 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 CoreModInfo { + + public static final CoreModInfo EMPTY = new CoreModInfo(); + + private final Map>> coreMods; + private final boolean liteLoaderAsMod; + + private CoreModInfo() { + this.coreMods = Collections.emptyMap(); + this.liteLoaderAsMod = false; + } + + private CoreModInfo(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 EnumSet 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; + } + + /// @return whether this coremod supports Forge 1.5.2- + public boolean isLegacy() { + return !getModLoaders(GameVersionNumber.asGameVersion("1.5.2")).isEmpty(); + } + + @NotNull + public static CoreModInfo 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)) { + // I have no idea why LiteLoader tries to use TweakClass in 1.5.2, when it is not supported + if (tweakClass.equals("com.mumfrey.liteloader.launch.LiteLoaderTweaker")) 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 CoreModInfo(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..6f02b0c1bec 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, CoreModInfo coreModInfo) 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, coreModInfo); } @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..c3b4d977925 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, CoreModInfo coreModInfo) throws IOException { + return fromFile(modManager, modFile, tree, ModLoaderType.FORGE, coreModInfo); } - 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, CoreModInfo coreModInfo) throws IOException { + return fromFile(modManager, modFile, tree, ModLoaderType.NEO_FORGE, coreModInfo); } - 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, CoreModInfo coreModInfo) 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, coreModInfo); } catch (Exception ignored) { } } try { - return fromFile0("META-INF/mods.toml", modLoaderType, modManager, modFile, tree); + return fromFile0("META-INF/mods.toml", modLoaderType, modManager, modFile, tree, coreModInfo); } catch (Exception ignored) { } try { - return fromEmbeddedMod(modManager, modFile, tree, modLoaderType); + return fromEmbeddedMod(modManager, modFile, tree, modLoaderType, coreModInfo); } catch (Exception ignored) { } @@ -215,7 +212,8 @@ private static LocalModFile fromFile0( ModLoaderType modLoaderType, ModManager modManager, Path modFile, - ZipFileTree tree) throws IOException, JsonParseException { + ZipFileTree tree, + CoreModInfo coreModInfo) 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, coreModInfo); } - 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, CoreModInfo coreModInfo) 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,8 @@ 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); + CoreModInfo embeddedCoreModInfo = CoreModInfo.fromFile(modFile, embeddedTree); + return fromFile(modManager, modFile, embeddedTree, modLoaderType, embeddedCoreModInfo); } catch (Exception ignored) { } } @@ -318,7 +317,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 +326,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..ab0c3c7d986 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, CoreModInfo coreModInfo) 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(), coreModInfo); } } 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..82515363063 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, CoreModInfo coreModInfo) 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(), "", coreModInfo); } } 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..719af170286 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, CoreModInfo coreModInfo) 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, coreModInfo ); } } 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..160f8988bfa 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,11 @@ import org.jackhuang.hmcl.addon.LocalAddonManager; import org.jackhuang.hmcl.addon.RemoteAddon; import org.jackhuang.hmcl.addon.RemoteAddonRepository; +import org.jackhuang.hmcl.addon.meta.CoreModInfo; import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.util.io.FileUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.nio.file.Files; @@ -50,13 +53,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, CoreModInfo coreModInfo) throws IOException, JsonParseException; } private static final Map>> READERS; @@ -105,7 +105,16 @@ public boolean hasMod(String modId, ModLoaderType modLoaderType) { } } - private void addModInfo(Path file) { + public boolean hasLiteLoaderAsMod() { + lock.lock(); + try { + return getLocalMod("liteloader-forge", ModLoaderType.FORGE).getFiles().stream().anyMatch(LocalModFile::isActive); + } finally { + lock.unlock(); + } + } + + private void addModInfo(Path file, boolean coreModOnly) { String fileName = StringUtils.removeSuffix(FileUtils.getName(file), DISABLED_EXTENSION, OLD_EXTENSION); String extension = fileName.substring(fileName.lastIndexOf(".") + 1); @@ -129,37 +138,57 @@ private void addModInfo(Path file) { } LocalModFile modInfo = null; + CoreModInfo coreModInfo = CoreModInfo.EMPTY; List exceptions = new ArrayList<>(); try (ZipFileTree tree = CompressingUtils.openZipTree(file)) { - for (ModMetadataReader reader : supportedReaders) { - try { - modInfo = reader.fromFile(this, file, tree); - break; - } catch (Exception e) { - exceptions.add(e); - } + try { + coreModInfo = CoreModInfo.fromFile(file, tree); + } catch (Exception e) { + exceptions.add(e); } - if (modInfo == null) { - for (ModMetadataReader reader : unsupportedReaders) { + if (coreModInfo.isLiteLoaderAsMod()) { + modInfo = new LocalModFile(this, + getLocalMod("liteloader-forge", ModLoaderType.FORGE), + file, + "LiteLoader", + new LocalAddonFile.Description("LiteLoader working together with Forge as a mod"), + coreModInfo + ); + } else if (!coreModOnly) { + for (ModMetadataReader reader : supportedReaders) { try { - modInfo = reader.fromFile(this, file, tree); + modInfo = reader.fromFile(this, file, tree, coreModInfo); break; - } catch (Exception ignored) { + } catch (Exception e) { + exceptions.add(e); + } + } + + if (modInfo == null) { + for (ModMetadataReader reader : unsupportedReaders) { + try { + modInfo = reader.fromFile(this, file, tree, coreModInfo); + break; + } catch (Exception ignored) { + } } } } } catch (Exception e) { + exceptions.add(e); LOG.warning("Failed to open mod file " + file, e); } if (modInfo == null) { - Exception exception = new Exception("Failed to read mod metadata"); - for (Exception e : exceptions) { - exception.addSuppressed(e); + if (!exceptions.isEmpty()) { + Exception exception = new Exception("Failed to read mod metadata"); + for (Exception e : exceptions) { + exception.addSuppressed(e); + } + LOG.warning("Failed to read mod metadata", exception); } - LOG.warning("Failed to read mod metadata", exception); String fileNameWithoutExtension = FileUtils.getNameWithoutExtension(file); @@ -167,7 +196,8 @@ 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" : ""), + coreModInfo ); } @@ -196,15 +226,25 @@ public void refresh() throws IOException { if (supportSubfolders && Files.isDirectory(subitem) && !".connector".equalsIgnoreCase(subitem.getFileName().toString())) { try (DirectoryStream subitemDirectoryStream = Files.newDirectoryStream(subitem)) { for (Path subsubitem : subitemDirectoryStream) { - addModInfo(subsubitem); + addModInfo(subsubitem, false); } } } else { - addModInfo(subitem); + addModInfo(subitem, false); } } } } + + var coreModsDir = instance.getCoreModsDirectory(); + if (coreModsDir != null) { + try (DirectoryStream coreModsDirectoryStream = Files.newDirectoryStream(coreModsDir)) { + for (Path item : coreModsDirectoryStream) { + addModInfo(item, true); + } + } + } + loaded = true; } finally { lock.unlock(); @@ -236,13 +276,19 @@ public void addMod(Path file) throws IOException { if (!loaded) refresh(); - Path modsDirectory = getDirectory(); - Files.createDirectories(modsDirectory); + boolean isLegacyCoreMod; + try (var tree = CompressingUtils.openZipTree(file)) { + isLegacyCoreMod = CoreModInfo.fromFile(file, tree).isLegacy(); + } + + Path coreModsDir = instance.getCoreModsDirectory(); + Path dir = isLegacyCoreMod && coreModsDir != null ? coreModsDir : getDirectory(); + Files.createDirectories(dir); - Path newFile = modsDirectory.resolve(file.getFileName()); + Path newFile = dir.resolve(file.getFileName()); FileUtils.copyFile(file, newFile); - addModInfo(newFile); + addModInfo(newFile, isLegacyCoreMod); } finally { lock.unlock(); } @@ -326,34 +372,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/game/GameInstance.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java index a558a8792b0..963f36cc22d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java @@ -135,6 +135,12 @@ default Path getModsDirectory() { return getRunDirectory().resolve("mods"); } + default @Nullable Path getCoreModsDirectory() { + return getVersion().isAtLeast("1.6.1", "13w36a") + ? null + : getRunDirectory().resolve("coremods"); + } + /// Returns the directory containing resource packs used by this instance. /// /// @return the resource pack directory below the run directory 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() {