Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
private ModManager modManager;
private HMCLGameRepository repository;
private GameInstanceID instanceId;
private String gameVersion;
String gameVersion;

final EnumSet<ModLoaderType> supportedLoaders = EnumSet.noneOf(ModLoaderType.class);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import javafx.util.Duration;
import org.jackhuang.hmcl.addon.RemoteAddon;
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
import org.jackhuang.hmcl.addon.meta.CoreMod;
import org.jackhuang.hmcl.addon.mod.LocalModFile;
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
Expand All @@ -63,6 +64,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;

Expand Down Expand Up @@ -628,18 +630,44 @@ 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<ModLoaderType> 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(CoreMod.getSupportedLoaders(modInfo.getCoreMods(), 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");
if (checkCoreModsDir)
warning.add(i18n("mods.coremods.check_dir"));
}
}
}

Expand Down
1 change: 1 addition & 0 deletions HMCL/src/main/resources/assets/lang/I18N.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions HMCL/src/main/resources/assets/lang/I18N_zh.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
142 changes: 142 additions & 0 deletions HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/CoreMod.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
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.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
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 org.jetbrains.annotations.Unmodifiable;

import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

import static org.jackhuang.hmcl.util.logging.Logger.LOG;

/// @see <a href="https://github.com/xfl03/CoreModTutor">CoreModTutor by xfl03 and contributors</a>
public record CoreMod(ModLoaderType modLoaderType, VersionRange<GameVersionNumber> 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));
}

@NotNull
@Unmodifiable
public static List<CoreMod> fromFile(Path modFile, ZipFileTree tree) {
if (!"jar".equalsIgnoreCase(FileUtils.getExtension(modFile))) return List.of();
List<CoreMod> coreMods = new ArrayList<>();
{
// 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))
coreMods.add(new CoreMod(ModLoaderType.FORGE, "1.3.2", "1.12.2"));
if (StringUtils.isNotBlank(tweakClass))
coreMods.addAll(List.of(
new CoreMod(ModLoaderType.FORGE, "1.6.1", "1.12.2"),
new CoreMod(ModLoaderType.LITE_LOADER, "1.6.1", "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<Map<String, String>>() {
});
if (map != null && !map.isEmpty())
coreMods.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)
coreMods.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)
coreMods.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)
coreMods.add(new CoreMod(ModLoaderType.NEO_FORGE, "1.21.9"));
}
return List.copyOf(coreMods);
}

@NotNull
public static EnumSet<ModLoaderType> getSupportedLoaders(List<CoreMod> coreMods, GameVersionNumber gameVersion) {
EnumSet<ModLoaderType> supportedLoaders = EnumSet.noneOf(ModLoaderType.class);
if (gameVersion == GameVersionNumber.unknown()) return supportedLoaders;
for (var coreMod : coreMods)
if (coreMod.mcVersionRange().contains(gameVersion))
supportedLoaders.add(coreMod.modLoaderType());
return supportedLoaders;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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, List<CoreMod> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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, List<CoreMod> 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, List<CoreMod> 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, List<CoreMod> 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) {
}

Expand All @@ -215,7 +212,8 @@ private static LocalModFile fromFile0(
ModLoaderType modLoaderType,
ModManager modManager,
Path modFile,
ZipFileTree tree) throws IOException, JsonParseException {
ZipFileTree tree,
List<CoreMod> 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.");
Expand Down Expand Up @@ -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, List<CoreMod> coreMods) throws IOException {
ZipArchiveEntry manifestFile = tree.getEntry("META-INF/MANIFEST.MF");
if (manifestFile == null)
throw new IOException("Missing MANIFEST.MF in file " + modFile);
Expand Down Expand Up @@ -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) {
}
}
Expand All @@ -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) {
Expand All @@ -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)
Expand Down
Loading