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 @@ -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;

Expand Down Expand Up @@ -551,9 +552,9 @@ final class ModInfoListCell extends MDListCell<ModInfoObject> {
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<ModInfoObject> listView) {
super(listView);
Expand Down Expand Up @@ -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<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(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");
}
}

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
165 changes: 165 additions & 0 deletions HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/CoreMods.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* 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.Immutable;
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 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 <a href="https://github.com/xfl03/CoreModTutor">CoreModTutor by xfl03 and contributors</a>
@Immutable
public final class CoreMods {

public static final CoreMods EMPTY = new CoreMods();

private final Map<ModLoaderType, List<VersionRange<GameVersionNumber>>> coreMods;

private CoreMods() {
this.coreMods = Collections.emptyMap();
}

private CoreMods(List<CoreMod> coreModList) {
EnumMap<ModLoaderType, List<VersionRange<GameVersionNumber>>> map = new EnumMap<>(ModLoaderType.class);
for (var coreMod : coreModList) {
map.computeIfAbsent(coreMod.modLoaderType(), k -> new ArrayList<>()).add(coreMod.mcVersionRange());
}
this.coreMods = Collections.unmodifiableMap(map);
}

public boolean isEmpty() {
return coreMods.isEmpty();
}

public Set<ModLoaderType> getModLoaders(GameVersionNumber gameVersionNumber) {
EnumSet<ModLoaderType> 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) {
if (!"jar".equalsIgnoreCase(FileUtils.getExtension(modFile))) return EMPTY;
List<CoreMod> coreModList = 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))
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))
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<Map<String, String>>() {
});
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);
}

private 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));
}

}
}
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, 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)
Expand Down
Loading