diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java index 2609468b9..2faa26e38 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -279,6 +279,8 @@ public class HMCLGameRepository extends DefaultGameRepository { return newBuiltinImage("/assets/img/fabric.png"); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FORGE)) return newBuiltinImage("/assets/img/forge.png"); + else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE)) + return newBuiltinImage("/assets/img/neoforge.png"); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.QUILT)) return newBuiltinImage("/assets/img/quilt.png"); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.OPTIFINE)) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java index c547ef37c..13989d727 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -510,9 +510,7 @@ public final class LauncherHelper { break; case MODDED_JAVA_16: // Minecraft<=1.17.1+Forge[37.0.0,37.0.60) not compatible with Java 17 - String forgePatchVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.FORGE) - .map(LibraryAnalyzer.LibraryType.FORGE::patchVersion) - .orElse(null); + String forgePatchVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.FORGE).orElse(null); if (forgePatchVersion != null && VersionNumber.VERSION_COMPARATOR.compare(forgePatchVersion, "37.0.60") < 0) suggestions.add(i18n("launch.advice.forge37_0_60")); else diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionIconType.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionIconType.java index d641472f1..1a9c41869 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionIconType.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionIconType.java @@ -27,6 +27,7 @@ public enum VersionIconType { CRAFT_TABLE("/assets/img/craft_table.png"), FABRIC("/assets/img/fabric.png"), FORGE("/assets/img/forge.png"), + NEO_FORGE("/assets/img/neoforge.png"), FURNACE("/assets/img/furnace.png"), QUILT("/assets/img/quilt.png"); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerItem.java index d5c8a11f9..7bc6a2b82 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerItem.java @@ -92,6 +92,9 @@ public class InstallerItem extends Control { case "quilt-api": imageUrl = "/assets/img/quilt.png"; break; + case "neoforge": + imageUrl = "/assets/img/neoforge.png"; + break; default: imageUrl = null; break; @@ -122,6 +125,7 @@ public class InstallerItem extends Control { public final InstallerItem fabric = new InstallerItem(FABRIC); public final InstallerItem fabricApi = new InstallerItem(FABRIC_API); public final InstallerItem forge = new InstallerItem(FORGE); + public final InstallerItem neoForge = new InstallerItem(NEO_FORGE); public final InstallerItem liteLoader = new InstallerItem(LITELOADER); public final InstallerItem optiFine = new InstallerItem(OPTIFINE); public final InstallerItem quilt = new InstallerItem(QUILT); @@ -131,30 +135,41 @@ public class InstallerItem extends Control { forge.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> { if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId(); if (quilt.libraryVersion.get() != null) return QUILT.getPatchId(); + if (neoForge.libraryVersion.get() != null) return NEO_FORGE.getPatchId(); return null; - }, fabric.libraryVersion, quilt.libraryVersion)); + }, fabric.libraryVersion, quilt.libraryVersion, neoForge.libraryVersion)); + + neoForge.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> { + if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId(); + if (quilt.libraryVersion.get() != null) return QUILT.getPatchId(); + if (forge.libraryVersion.get() != null) return FORGE.getPatchId(); + return null; + }, fabric.libraryVersion, quilt.libraryVersion, forge.libraryVersion)); liteLoader.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> { if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId(); if (quilt.libraryVersion.get() != null) return QUILT.getPatchId(); + if (neoForge.libraryVersion.get() != null) return NEO_FORGE.getPatchId(); return null; - }, fabric.libraryVersion, quilt.libraryVersion)); + }, fabric.libraryVersion, quilt.libraryVersion, neoForge.libraryVersion)); optiFine.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> { if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId(); if (quilt.libraryVersion.get() != null) return QUILT.getPatchId(); + if (neoForge.libraryVersion.get() != null) return NEO_FORGE.getPatchId(); return null; - }, fabric.libraryVersion, quilt.libraryVersion)); + }, fabric.libraryVersion, quilt.libraryVersion, neoForge.libraryVersion)); for (InstallerItem fabric : new InstallerItem[]{fabric, fabricApi}) { fabric.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> { if (forge.libraryVersion.get() != null) return FORGE.getPatchId(); + if (neoForge.libraryVersion.get() != null) return NEO_FORGE.getPatchId(); if (liteLoader.libraryVersion.get() != null) return LITELOADER.getPatchId(); if (optiFine.libraryVersion.get() != null) return OPTIFINE.getPatchId(); if (quilt.libraryVersion.get() != null) return QUILT.getPatchId(); if (quiltApi.libraryVersion.get() != null) return QUILT_API.getPatchId(); return null; - }, forge.libraryVersion, liteLoader.libraryVersion, optiFine.libraryVersion, quilt.libraryVersion, quiltApi.libraryVersion)); + }, forge.libraryVersion, neoForge.libraryVersion, liteLoader.libraryVersion, optiFine.libraryVersion, quilt.libraryVersion, quiltApi.libraryVersion)); } fabricApi.dependencyName.bind(Bindings.createStringBinding(() -> { @@ -167,10 +182,11 @@ public class InstallerItem extends Control { if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId(); if (fabricApi.libraryVersion.get() != null) return FABRIC_API.getPatchId(); if (forge.libraryVersion.get() != null) return FORGE.getPatchId(); + if (neoForge.libraryVersion.get() != null) return NEO_FORGE.getPatchId(); if (liteLoader.libraryVersion.get() != null) return LITELOADER.getPatchId(); if (optiFine.libraryVersion.get() != null) return OPTIFINE.getPatchId(); return null; - }, fabric.libraryVersion, fabricApi.libraryVersion, forge.libraryVersion, liteLoader.libraryVersion, optiFine.libraryVersion)); + }, fabric.libraryVersion, fabricApi.libraryVersion, forge.libraryVersion, neoForge.libraryVersion, liteLoader.libraryVersion, optiFine.libraryVersion)); } quiltApi.dependencyName.bind(Bindings.createStringBinding(() -> { @@ -180,7 +196,7 @@ public class InstallerItem extends Control { } public InstallerItem[] getLibraries() { - return new InstallerItem[]{game, forge, liteLoader, optiFine, fabric, fabricApi, quilt, quiltApi}; + return new InstallerItem[]{game, forge, neoForge, liteLoader, optiFine, fabric, fabricApi, quilt, quiltApi}; } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java index 1d12e0f17..e9fd30437 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java @@ -36,6 +36,7 @@ import org.jackhuang.hmcl.download.game.GameAssetDownloadTask; import org.jackhuang.hmcl.download.game.GameInstallTask; import org.jackhuang.hmcl.download.java.JavaDownloadTask; import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask; +import org.jackhuang.hmcl.download.neoforge.NeoForgeInstallTask; import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask; import org.jackhuang.hmcl.download.quilt.QuiltAPIInstallTask; import org.jackhuang.hmcl.download.quilt.QuiltInstallTask; @@ -123,6 +124,8 @@ public final class TaskListPane extends StackPane { task.setName(i18n("install.installer.install", i18n("install.installer.game"))); } else if (task instanceof ForgeNewInstallTask || task instanceof ForgeOldInstallTask) { task.setName(i18n("install.installer.install", i18n("install.installer.forge"))); + } else if (task instanceof NeoForgeInstallTask) { + task.setName(i18n("install.installer.install", i18n("install.installer.neoforge"))); } else if (task instanceof LiteLoaderInstallTask) { task.setName(i18n("install.installer.install", i18n("install.installer.liteloader"))); } else if (task instanceof OptiFineInstallTask) { @@ -248,6 +251,7 @@ public final class TaskListPane extends StackPane { case "hmcl.install.assets": message = i18n("assets.download"); break; case "hmcl.install.game": message = i18n("install.installer.install", i18n("install.installer.game") + " " + stageValue); break; case "hmcl.install.forge": message = i18n("install.installer.install", i18n("install.installer.forge") + " " + stageValue); break; + case "hmcl.install.neoforge": message = i18n("install.installer.install", i18n("install.installer.neoforge") + " " + stageValue); break; case "hmcl.install.liteloader": message = i18n("install.installer.install", i18n("install.installer.liteloader") + " " + stageValue); break; case "hmcl.install.optifine": message = i18n("install.installer.install", i18n("install.installer.optifine") + " " + stageValue); break; case "hmcl.install.fabric": message = i18n("install.installer.install", i18n("install.installer.fabric") + " " + stageValue); break; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallersPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallersPage.java index 19235aa73..a36a2c09d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallersPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallersPage.java @@ -23,14 +23,12 @@ import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.control.Control; -import javafx.scene.control.Label; -import javafx.scene.control.Skin; -import javafx.scene.control.SkinBase; +import javafx.scene.control.*; import javafx.scene.layout.BorderPane; import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import org.jackhuang.hmcl.download.DownloadProvider; +import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.download.RemoteVersion; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.ui.Controllers; @@ -72,9 +70,9 @@ public class InstallersPage extends Control implements WizardPage { for (InstallerItem library : group.getLibraries()) { String libraryId = library.getLibraryId(); - if (libraryId.equals("game")) continue; + if (libraryId.equals(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId())) continue; library.action.set(e -> { - if ("fabric-api".equals(libraryId)) { + if (LibraryAnalyzer.LibraryType.FABRIC_API.getPatchId().equals(libraryId)) { Controllers.dialog(i18n("install.installer.fabric-api.warning"), i18n("message.warning"), MessageDialogPane.MessageType.WARNING); } @@ -155,10 +153,13 @@ public class InstallersPage extends Control implements WizardPage { { FlowPane libraryPane = new FlowPane(control.group.getLibraries()); - BorderPane.setMargin(libraryPane, new Insets(16, 0, 16, 0)); libraryPane.setVgap(16); libraryPane.setHgap(16); - root.setCenter(libraryPane); + ScrollPane scrollPane = new ScrollPane(libraryPane); + scrollPane.setFitToWidth(true); + scrollPane.setFitToHeight(true); + BorderPane.setMargin(scrollPane, new Insets(16, 0, 16, 0)); + root.setCenter(scrollPane); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java index 9bee8d246..e5b8311f6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java @@ -36,6 +36,7 @@ import org.jackhuang.hmcl.download.fabric.FabricRemoteVersion; import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion; import org.jackhuang.hmcl.download.game.GameRemoteVersion; import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersion; +import org.jackhuang.hmcl.download.neoforge.NeoForgeRemoteVersion; import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion; import org.jackhuang.hmcl.download.quilt.QuiltAPIRemoteVersion; import org.jackhuang.hmcl.download.quilt.QuiltRemoteVersion; @@ -335,6 +336,8 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres iconType = VersionIconType.COMMAND; else if (remoteVersion instanceof ForgeRemoteVersion) iconType = VersionIconType.FORGE; + else if (remoteVersion instanceof NeoForgeRemoteVersion) + iconType = VersionIconType.NEO_FORGE; else if (remoteVersion instanceof FabricRemoteVersion || remoteVersion instanceof FabricAPIRemoteVersion) iconType = VersionIconType.FABRIC; else if (remoteVersion instanceof QuiltRemoteVersion || remoteVersion instanceof QuiltAPIRemoteVersion) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index d968920b9..2a8f8a74f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -399,6 +399,9 @@ public class DownloadPage extends Control implements DecoratorPage { case FORGE: content.getTags().add(i18n("install.installer.forge")); break; + case NEO_FORGED: + content.getTags().add(i18n("install.installer.neoforge")); + break; case FABRIC: content.getTags().add(i18n("install.installer.fabric")); break; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerListPage.java index f3e66e86f..b5ebc9865 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerListPage.java @@ -86,7 +86,7 @@ public class InstallerListPage extends ListPageBase implements Ve InstallerItem.InstallerItemGroup group = new InstallerItem.InstallerItemGroup(); - // Conventional libraries: game, fabric, forge, liteloader, optifine + // Conventional libraries: game, fabric, forge, neoforge, liteloader, optifine for (InstallerItem installerItem : group.getLibraries()) { String libraryId = installerItem.getLibraryId(); String libraryVersion = analyzer.getVersion(libraryId).orElse(null); @@ -96,7 +96,7 @@ public class InstallerListPage extends ListPageBase implements Ve installerItem.action.set(e -> { Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, libraryId, libraryVersion)); }); - boolean removable = !"game".equals(libraryId) && libraryVersion != null; + boolean removable = !LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId().equals(libraryId) && libraryVersion != null; installerItem.removable.set(removable); if (removable) { Runnable action = removeAction.apply(libraryId); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java index d31b5c900..99f0d63ac 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java @@ -335,15 +335,28 @@ class ModListPageSkin extends SkinBase { RemoteMod remoteMod = remoteModRepository.getModById(versionOptional.get().getModid()); FXUtils.runInFX(() -> { for (ModLoaderType modLoaderType : versionOptional.get().getLoaders()) { + String loaderName; switch (modLoaderType) { - case FABRIC: case FORGE: + loaderName = i18n("install.installer.forge"); + break; + case NEO_FORGED: + loaderName = i18n("install.installer.neoforge"); + break; + case FABRIC: + loaderName = i18n("install.installer.fabric"); + break; case LITE_LOADER: - case QUILT: { - if (!title.getTags().contains(modLoaderType.getLoaderName())) { - title.getTags().add(modLoaderType.getLoaderName()); - } - } + loaderName = i18n("install.installer.liteloader"); + break; + case QUILT: + loaderName = i18n("install.installer.quilt"); + break; + default: + continue; + } + if (!title.getTags().contains(loaderName)) { + title.getTags().add(loaderName); } } @@ -457,7 +470,23 @@ class ModListPageSkin extends SkinBase { if (empty) return; content.setTitle(dataItem.getTitle()); content.getTags().clear(); - content.getTags().add(dataItem.getModInfo().getModLoaderType().getLoaderName()); + switch (dataItem.getModInfo().getModLoaderType()) { + case FORGE: + content.getTags().add(i18n("install.installer.forge")); + break; + case NEO_FORGED: + content.getTags().add(i18n("install.installer.neoforge")); + break; + case FABRIC: + content.getTags().add(i18n("install.installer.fabric")); + break; + case LITE_LOADER: + content.getTags().add(i18n("install.installer.liteloader")); + break; + case QUILT: + content.getTags().add(i18n("install.installer.quilt")); + break; + } if (dataItem.getMod() != null) { if (I18n.getCurrentLocale().getLocale() == Locale.CHINA) { content.getTags().add(dataItem.getMod().getDisplayName()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionIconDialog.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionIconDialog.java index 01f26789c..cde2d27f2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionIconDialog.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionIconDialog.java @@ -65,6 +65,7 @@ public class VersionIconDialog extends DialogPane { createIcon(VersionIconType.CRAFT_TABLE), createIcon(VersionIconType.FABRIC), createIcon(VersionIconType.FORGE), + createIcon(VersionIconType.NEO_FORGE), createIcon(VersionIconType.FURNACE), createIcon(VersionIconType.QUILT) ); diff --git a/HMCL/src/main/resources/assets/img/neoforge.png b/HMCL/src/main/resources/assets/img/neoforge.png new file mode 100644 index 000000000..95c3b5cf6 Binary files /dev/null and b/HMCL/src/main/resources/assets/img/neoforge.png differ diff --git a/HMCL/src/main/resources/assets/img/neoforged@2x.png b/HMCL/src/main/resources/assets/img/neoforged@2x.png new file mode 100644 index 000000000..7d46d392e Binary files /dev/null and b/HMCL/src/main/resources/assets/img/neoforged@2x.png differ diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index d826e81b4..8226870e7 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -617,6 +617,7 @@ install.installer.fabric=Fabric install.installer.fabric-api=Fabric API install.installer.fabric-api.warning=Warning: Fabric API is a mod, and will be installed into the mod directory of the game instance. Please do not change the working directory of the game, or the Fabric API will not work. If you do want to change these settings, you should reinstall it. install.installer.forge=Forge +install.installer.neoforge=NeoForge install.installer.game=Minecraft install.installer.incompatible=Incompatible with %s install.installer.install=Install %s @@ -838,6 +839,7 @@ modrinth.category.misc=Misc modrinth.category.mobs=Mobs modrinth.category.modloader=Modloader modrinth.category.multiplayer=Multiplayer +modrinth.category.neoforge=NeoForge modrinth.category.optimization=Optimization modrinth.category.paper=Paper modrinth.category.purpur=Purpur diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index f9a346feb..45c5e535b 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -497,6 +497,7 @@ install.installer.fabric=Fabric install.installer.fabric-api=Fabric API install.installer.fabric-api.warning=警告:Fabric API 是 Mod,將會被安裝到新遊戲的 Mod 資料夾,請你在安裝遊戲後不要修改當前遊戲的版本隔離/遊戲運行路徑設置,如果你在之後修改了相關設置,Fabric API 需要被重新安裝。 install.installer.forge=Forge +install.installer.neoforge=NeoForge install.installer.game=Minecraft install.installer.incompatible=與 %s 不相容 install.installer.install=安裝%s @@ -709,6 +710,7 @@ modrinth.category.misc=其他 modrinth.category.mobs=生物 modrinth.category.modloader=Modloader modrinth.category.multiplayer=多人 +modrinth.category.neoforge=NeoForge modrinth.category.optimization=最佳化 modrinth.category.paper=Paper modrinth.category.purpur=Purpur 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 8c0305037..95a2b0f98 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -496,6 +496,7 @@ install.installer.fabric=Fabric install.installer.fabric-api=Fabric API install.installer.fabric-api.warning=警告:Fabric API 是一个 Mod,将会被安装到新游戏的 Mod 文件夹,请你在安装游戏后不要修改当前游戏的版本隔离/游戏运行路径设置,如果你在之后修改了相关设置,Fabric API 需要被重新安装。 install.installer.forge=Forge +install.installer.neoforge=NeoForge install.installer.game=Minecraft install.installer.incompatible=与 %s 不兼容 install.installer.install=安装 %s @@ -708,6 +709,7 @@ modrinth.category.misc=其他 modrinth.category.mobs=生物 modrinth.category.modloader=Modloader modrinth.category.multiplayer=多人 +modrinth.category.neoforge=NeoForge modrinth.category.optimization=优化 modrinth.category.paper=Paper modrinth.category.purpur=Purpur diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java index 69aeebda7..bb7105900 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java @@ -22,6 +22,7 @@ import org.jackhuang.hmcl.download.fabric.FabricVersionList; import org.jackhuang.hmcl.download.forge.ForgeBMCLVersionList; import org.jackhuang.hmcl.download.game.GameVersionList; import org.jackhuang.hmcl.download.liteloader.LiteLoaderBMCLVersionList; +import org.jackhuang.hmcl.download.neoforge.NeoForgeBMCLVersionList; import org.jackhuang.hmcl.download.optifine.OptiFineBMCLVersionList; import org.jackhuang.hmcl.download.quilt.QuiltAPIVersionList; import org.jackhuang.hmcl.download.quilt.QuiltVersionList; @@ -36,6 +37,7 @@ public class BMCLAPIDownloadProvider implements DownloadProvider { private final FabricVersionList fabric; private final FabricAPIVersionList fabricApi; private final ForgeBMCLVersionList forge; + private final NeoForgeBMCLVersionList neoforge; private final LiteLoaderBMCLVersionList liteLoader; private final OptiFineBMCLVersionList optifine; private final QuiltVersionList quilt; @@ -47,6 +49,7 @@ public class BMCLAPIDownloadProvider implements DownloadProvider { this.fabric = new FabricVersionList(this); this.fabricApi = new FabricAPIVersionList(this); this.forge = new ForgeBMCLVersionList(apiRoot); + this.neoforge = new NeoForgeBMCLVersionList(apiRoot); this.liteLoader = new LiteLoaderBMCLVersionList(this); this.optifine = new OptiFineBMCLVersionList(apiRoot); this.quilt = new QuiltVersionList(this); @@ -78,6 +81,8 @@ public class BMCLAPIDownloadProvider implements DownloadProvider { return fabricApi; case "forge": return forge; + case "neoforge": + return neoforge; case "liteloader": return liteLoader; case "optifine": @@ -102,6 +107,7 @@ public class BMCLAPIDownloadProvider implements DownloadProvider { .replace("https://libraries.minecraft.net", apiRoot + "/libraries") .replaceFirst("https?://files\\.minecraftforge\\.net/maven", apiRoot + "/maven") .replace("https://maven.minecraftforge.net", apiRoot + "/maven") + .replace("https://maven.neoforged.net/releases/net/neoforged/forge", apiRoot + "/maven/net/neoforged/forge") .replace("http://dl.liteloader.com/versions/versions.json", apiRoot + "/maven/com/mumfrey/liteloader/versions.json") .replace("http://dl.liteloader.com/versions", apiRoot + "/maven") .replace("https://meta.fabricmc.net", apiRoot + "/fabric-meta") diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java index a492c66c2..559f6b70f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -21,6 +21,7 @@ import org.jackhuang.hmcl.download.forge.ForgeInstallTask; import org.jackhuang.hmcl.download.game.GameAssetDownloadTask; import org.jackhuang.hmcl.download.game.GameDownloadTask; import org.jackhuang.hmcl.download.game.GameLibrariesTask; +import org.jackhuang.hmcl.download.neoforge.NeoForgeInstallTask; import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask; import org.jackhuang.hmcl.game.Artifact; import org.jackhuang.hmcl.game.DefaultGameRepository; @@ -179,6 +180,11 @@ public class DefaultDependencyManager extends AbstractDependencyManager { return Task .composeAsync(() -> { + try { + return NeoForgeInstallTask.install(this, oldVersion, installer); + } catch (IOException ignore) { + } + try { return ForgeInstallTask.install(this, oldVersion, installer); } catch (IOException ignore) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LibraryAnalyzer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LibraryAnalyzer.java index e1d78f0f2..1d664484a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LibraryAnalyzer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LibraryAnalyzer.java @@ -17,9 +17,7 @@ */ package org.jackhuang.hmcl.download; -import org.jackhuang.hmcl.game.Library; -import org.jackhuang.hmcl.game.Version; -import org.jackhuang.hmcl.game.VersionProvider; +import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.mod.ModLoaderType; import org.jackhuang.hmcl.util.Pair; import org.jetbrains.annotations.NotNull; @@ -113,7 +111,8 @@ public final class LibraryAnalyzer implements Iterable[0-9.]+)(-([0-9.]+))?$"); @Override - public String patchVersion(String libraryVersion) { + public String patchVersion(Version gameVersion, String libraryVersion) { Matcher matcher = FORGE_VERSION_MATCHER.matcher(libraryVersion); if (matcher.find()) { return matcher.group("forge"); } - return super.patchVersion(libraryVersion); + return super.patchVersion(gameVersion, libraryVersion); } }, + NEO_FORGE(true, "neoforge", Pattern.compile("net\\.neoforged\\.fancymodloader"), Pattern.compile("(core|loader)"), ModLoaderType.NEO_FORGED) { + @Override + public String patchVersion(Version gameVersion, String libraryVersion) { + String res = scanVersion(gameVersion); + if (res != null) { + return res; + } + + for (Version patch : gameVersion.getPatches()) { + res = scanVersion(patch); + if (res != null) { + return res; + } + } + + return super.patchVersion(gameVersion, libraryVersion); + } + + private String scanVersion(Version version) { + Optional optArgument = version.getArguments(); + if (!optArgument.isPresent()) { + return null; + } + List gameArguments = optArgument.get().getGame(); + if (gameArguments == null) { + return null; + } + + for (int i = 0; i < gameArguments.size() - 1; i++) { + Argument argument = gameArguments.get(i); + if (argument instanceof StringArgument && "--fml.neoForgeVersion".equals(((StringArgument) argument).getArgument())) { + Argument next = gameArguments.get(i + 1); + if (next instanceof StringArgument) { + return ((StringArgument) next).getArgument(); + } + return null; // Normally, there should not be two --fml.neoForgeVersion argument. + } + } + return null; + } + + }, LITELOADER(true, "liteloader", Pattern.compile("com\\.mumfrey"), Pattern.compile("liteloader"), ModLoaderType.LITE_LOADER), OPTIFINE(false, "optifine", Pattern.compile("(net\\.)?optifine"), Pattern.compile("^(?!.*launchwrapper).*$"), null), QUILT(true, "quilt", Pattern.compile("org\\.quiltmc"), Pattern.compile("quilt-loader"), ModLoaderType.QUILT), @@ -229,7 +270,7 @@ public final class LibraryAnalyzer implements Iterable { LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version); VersionLibraryBuilder builder = new VersionLibraryBuilder(version); - if (!libraryAnalyzer.has(FORGE)) return version; + if (!libraryAnalyzer.has(FORGE) && !libraryAnalyzer.has(NEO_FORGE)) return version; Optional bslVersion = libraryAnalyzer.getVersion(BOOTSTRAP_LAUNCHER); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MojangDownloadProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MojangDownloadProvider.java index 34736b4c8..33d11327e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MojangDownloadProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MojangDownloadProvider.java @@ -22,19 +22,21 @@ import org.jackhuang.hmcl.download.fabric.FabricVersionList; import org.jackhuang.hmcl.download.forge.ForgeBMCLVersionList; import org.jackhuang.hmcl.download.game.GameVersionList; import org.jackhuang.hmcl.download.liteloader.LiteLoaderVersionList; +import org.jackhuang.hmcl.download.neoforge.NeoForgeBMCLVersionList; import org.jackhuang.hmcl.download.optifine.OptiFineBMCLVersionList; import org.jackhuang.hmcl.download.quilt.QuiltAPIVersionList; import org.jackhuang.hmcl.download.quilt.QuiltVersionList; /** - * @see http://wiki.vg * @author huangyuhui + * @see http://wiki.vg */ public class MojangDownloadProvider implements DownloadProvider { private final GameVersionList game; private final FabricVersionList fabric; private final FabricAPIVersionList fabricApi; private final ForgeBMCLVersionList forge; + private final NeoForgeBMCLVersionList neoforge; private final LiteLoaderVersionList liteLoader; private final OptiFineBMCLVersionList optifine; private final QuiltVersionList quilt; @@ -47,6 +49,7 @@ public class MojangDownloadProvider implements DownloadProvider { this.fabric = new FabricVersionList(this); this.fabricApi = new FabricAPIVersionList(this); this.forge = new ForgeBMCLVersionList(apiRoot); + this.neoforge = new NeoForgeBMCLVersionList(apiRoot); this.liteLoader = new LiteLoaderVersionList(this); this.optifine = new OptiFineBMCLVersionList(apiRoot); this.quilt = new QuiltVersionList(this); @@ -74,6 +77,8 @@ public class MojangDownloadProvider implements DownloadProvider { return fabricApi; case "forge": return forge; + case "neoforge": + return neoforge; case "liteloader": return liteLoader; case "optifine": diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallTask.java index 7fc158492..8c96ba321 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallTask.java @@ -192,7 +192,7 @@ public class ForgeNewInstallTask extends Task { private Path tempDir; private AtomicInteger processorDoneCount = new AtomicInteger(0); - ForgeNewInstallTask(DefaultDependencyManager dependencyManager, Version version, String selfVersion, Path installer) { + public ForgeNewInstallTask(DefaultDependencyManager dependencyManager, Version version, String selfVersion, Path installer) { this.dependencyManager = dependencyManager; this.gameRepository = dependencyManager.getGameRepository(); this.version = version; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeBMCLVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeBMCLVersionList.java new file mode 100644 index 000000000..cf4cee35b --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeBMCLVersionList.java @@ -0,0 +1,136 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2021 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.download.neoforge; + +import com.google.gson.JsonParseException; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import org.jackhuang.hmcl.download.VersionList; +import org.jackhuang.hmcl.util.Immutable; +import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.gson.Validation; +import org.jackhuang.hmcl.util.io.HttpRequest; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import static org.jackhuang.hmcl.util.Lang.wrap; + +public final class NeoForgeBMCLVersionList extends VersionList { + private final String apiRoot; + + /** + * @param apiRoot API Root of BMCLAPI implementations + */ + public NeoForgeBMCLVersionList(String apiRoot) { + this.apiRoot = apiRoot; + } + + @Override + public boolean hasType() { + return false; + } + + @Override + public CompletableFuture loadAsync() { + throw new UnsupportedOperationException("NeoForgeBMCLVersionList does not support loading the entire NeoForge remote version list."); + } + + @Override + public CompletableFuture refreshAsync() { + throw new UnsupportedOperationException("NeoForgeBMCLVersionList does not support loading the entire NeoForge remote version list."); + } + + @Override + public CompletableFuture refreshAsync(String gameVersion) { + return CompletableFuture.completedFuture((Void) null) + .thenApplyAsync(wrap(unused -> HttpRequest.GET(apiRoot + "/neoforge/list/" + gameVersion).>getJson(new TypeToken>() { + }.getType()))) + .thenAcceptAsync(neoForgeVersions -> { + lock.writeLock().lock(); + + try { + versions.clear(gameVersion); + for (NeoForgeVersion neoForgeVersion : neoForgeVersions) { + String nf = StringUtils.removePrefix( + neoForgeVersion.version, + "1.20.1".equals(gameVersion) ? "1.20.1-forge-" : "neoforge-" // Som of the version numbers for 1.20.1 are like forge. + ); + versions.put(gameVersion, new NeoForgeRemoteVersion( + neoForgeVersion.mcVersion, + nf, + Lang.immutableListOf( + apiRoot + "/neoforge/version/" + neoForgeVersion.version + "/download/installer.jar" + ) + )); + } + } finally { + lock.writeLock().unlock(); + } + }); + } + + @Override + public Optional getVersion(String gameVersion, String remoteVersion) { + remoteVersion = StringUtils.substringAfter(remoteVersion, "-", remoteVersion); + return super.getVersion(gameVersion, remoteVersion); + } + + @Immutable + private static final class NeoForgeVersion implements Validation { + private final String rawVersion; + + private final String version; + + @SerializedName("mcversion") + private final String mcVersion; + + public NeoForgeVersion(String rawVersion, String version, String mcVersion) { + this.rawVersion = rawVersion; + this.version = version; + this.mcVersion = mcVersion; + } + + public String getRawVersion() { + return this.rawVersion; + } + + public String getVersion() { + return this.version; + } + + public String getMcVersion() { + return this.mcVersion; + } + + @Override + public void validate() throws JsonParseException { + if (this.rawVersion == null) { + throw new JsonParseException("NeoForgeVersion rawVersion cannot be null."); + } + if (this.version == null) { + throw new JsonParseException("NeoForgeVersion version cannot be null."); + } + if (this.mcVersion == null) { + throw new JsonParseException("NeoForgeVersion mcversion cannot be null."); + } + } + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeInstallTask.java new file mode 100644 index 000000000..58305192b --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeInstallTask.java @@ -0,0 +1,120 @@ +package org.jackhuang.hmcl.download.neoforge; + +import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.download.LibraryAnalyzer; +import org.jackhuang.hmcl.download.VersionMismatchException; +import org.jackhuang.hmcl.download.forge.*; +import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.task.FileDownloadTask; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.io.CompressingUtils; +import org.jackhuang.hmcl.util.io.FileUtils; + +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; + +import static org.jackhuang.hmcl.util.StringUtils.removePrefix; +import static org.jackhuang.hmcl.util.StringUtils.removeSuffix; + +public final class NeoForgeInstallTask extends Task { + private final DefaultDependencyManager dependencyManager; + + private final Version version; + + private final NeoForgeRemoteVersion remoteVersion; + + private Path installer = null; + + private FileDownloadTask dependent; + + private Task dependency; + + public NeoForgeInstallTask(DefaultDependencyManager dependencyManager, Version version, NeoForgeRemoteVersion remoteVersion) { + this.dependencyManager = dependencyManager; + this.version = version; + this.remoteVersion = remoteVersion; + } + + @Override + public boolean doPreExecute() { + return true; + } + + @Override + public void preExecute() throws Exception { + installer = Files.createTempFile("neoforge-installer", ".jar"); + + dependent = new FileDownloadTask( + dependencyManager.getDownloadProvider().injectURLsWithCandidates(remoteVersion.getUrls()), + installer.toFile(), null + ); + dependent.setCacheRepository(dependencyManager.getCacheRepository()); + dependent.setCaching(true); + dependent.addIntegrityCheckHandler(FileDownloadTask.ZIP_INTEGRITY_CHECK_HANDLER); + } + + @Override + public boolean doPostExecute() { + return true; + } + + @Override + public void postExecute() throws Exception { + Files.deleteIfExists(installer); + this.setResult(dependency.getResult()); + } + + @Override + public Collection> getDependents() { + return Collections.singleton(dependent); + } + + @Override + public Collection> getDependencies() { + return Collections.singleton(dependency); + } + + @Override + public void execute() throws Exception { + dependency = install(dependencyManager, version, installer); + } + + public static Task install(DefaultDependencyManager dependencyManager, Version version, Path installer) throws IOException, VersionMismatchException { + Optional gameVersion = dependencyManager.getGameRepository().getGameVersion(version); + if (!gameVersion.isPresent()) throw new IOException(); + try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) { + String installProfileText = FileUtils.readText(fs.getPath("install_profile.json")); + Map installProfile = JsonUtils.fromNonNullJson(installProfileText, Map.class); + if (LibraryAnalyzer.LibraryType.FORGE.getPatchId().equals(installProfile.get("profile")) && Files.exists(fs.getPath("META-INF/NEOFORGE.RSA"))) { + ForgeNewInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeNewInstallProfile.class); + if (!gameVersion.get().equals(profile.getMinecraft())) + throw new VersionMismatchException(profile.getMinecraft(), gameVersion.get()); + return new ForgeNewInstallTask(dependencyManager, version, modifyNeoForgeOldVersion(gameVersion.get(), profile.getVersion()), installer).thenApplyAsync(neoForgeVersion -> { + if (!neoForgeVersion.getId().equals(LibraryAnalyzer.LibraryType.FORGE.getPatchId()) || neoForgeVersion.getVersion() == null) { + throw new IOException("Invalid neoforge version."); + } + return neoForgeVersion.setId(LibraryAnalyzer.LibraryType.NEO_FORGE.getPatchId()).setVersion(neoForgeVersion.getVersion().replace(LibraryAnalyzer.LibraryType.FORGE.getPatchId(), LibraryAnalyzer.LibraryType.NEO_FORGE.getPatchId())); + }); + } else if (LibraryAnalyzer.LibraryType.NEO_FORGE.getPatchId().equals(installProfile.get("profile"))) { + ForgeNewInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeNewInstallProfile.class); + if (!gameVersion.get().equals(profile.getMinecraft())) + throw new VersionMismatchException(profile.getMinecraft(), gameVersion.get()); + return new NeoForgeOldInstallTask(dependencyManager, version, modifyNeoForgeNewVersion(profile.getVersion()), installer); + } else { + throw new IOException(); + } + } + } + + private static String modifyNeoForgeOldVersion(String gameVersion, String version) { + return removeSuffix(removePrefix(removeSuffix(removePrefix(version.replace(gameVersion, "").trim(), "-"), "-"), "_"), "_"); + } + + private static String modifyNeoForgeNewVersion(String version) { + return removePrefix(version.replace("neoforge", ""), "-"); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeOldInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeOldInstallTask.java new file mode 100644 index 000000000..3de56d9df --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeOldInstallTask.java @@ -0,0 +1,424 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2021 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.download.neoforge; + +import org.jackhuang.hmcl.download.ArtifactMalformedException; +import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.download.LibraryAnalyzer; +import org.jackhuang.hmcl.download.forge.ForgeNewInstallProfile; +import org.jackhuang.hmcl.download.forge.ForgeNewInstallProfile.Processor; +import org.jackhuang.hmcl.download.game.GameLibrariesTask; +import org.jackhuang.hmcl.download.game.VersionJsonDownloadTask; +import org.jackhuang.hmcl.game.*; +import org.jackhuang.hmcl.task.FileDownloadTask; +import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.util.DigestUtils; +import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.function.ExceptionalFunction; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.io.ChecksumMismatchException; +import org.jackhuang.hmcl.util.io.CompressingUtils; +import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.util.platform.CommandBuilder; +import org.jackhuang.hmcl.util.platform.JavaVersion; +import org.jackhuang.hmcl.util.platform.OperatingSystem; +import org.jackhuang.hmcl.util.platform.SystemUtils; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.zip.ZipException; + +import static org.jackhuang.hmcl.util.Logging.LOG; +import static org.jackhuang.hmcl.util.gson.JsonUtils.fromNonNullJson; + +public class NeoForgeOldInstallTask extends Task { + + private class ProcessorTask extends Task { + + private final Processor processor; + private final Map vars; + + public ProcessorTask(@NotNull Processor processor, @NotNull Map vars) { + this.processor = processor; + this.vars = vars; + setSignificance(TaskSignificance.MODERATE); + } + + @Override + public void execute() throws Exception { + Map outputs = new HashMap<>(); + boolean miss = false; + + for (Map.Entry entry : processor.getOutputs().entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + + key = parseLiteral(key, vars); + value = parseLiteral(value, vars); + + if (key == null || value == null) { + throw new ArtifactMalformedException("Invalid forge installation configuration"); + } + + outputs.put(key, value); + + Path artifact = Paths.get(key); + if (Files.exists(artifact)) { + String code; + try (InputStream stream = Files.newInputStream(artifact)) { + code = (DigestUtils.digestToString("SHA-1", stream)); + } + + if (!Objects.equals(code, value)) { + Files.delete(artifact); + LOG.info("Found existing file is not valid: " + artifact); + + miss = true; + } + } else { + miss = true; + } + } + + if (!processor.getOutputs().isEmpty() && !miss) { + return; + } + + Path jar = gameRepository.getArtifactFile(version, processor.getJar()); + if (!Files.isRegularFile(jar)) + throw new FileNotFoundException("Game processor file not found, should be downloaded in preprocess"); + + String mainClass; + try (JarFile jarFile = new JarFile(jar.toFile())) { + mainClass = jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.MAIN_CLASS); + } + + if (StringUtils.isBlank(mainClass)) + throw new Exception("Game processor jar does not have main class " + jar); + + List command = new ArrayList<>(); + command.add(JavaVersion.fromCurrentEnvironment().getBinary().toString()); + command.add("-cp"); + + List classpath = new ArrayList<>(processor.getClasspath().size() + 1); + for (Artifact artifact : processor.getClasspath()) { + Path file = gameRepository.getArtifactFile(version, artifact); + if (!Files.isRegularFile(file)) + throw new Exception("Game processor dependency missing"); + classpath.add(file.toString()); + } + classpath.add(jar.toString()); + command.add(String.join(OperatingSystem.PATH_SEPARATOR, classpath)); + + command.add(mainClass); + + List args = new ArrayList<>(processor.getArgs().size()); + for (String arg : processor.getArgs()) { + String parsed = parseLiteral(arg, vars); + if (parsed == null) + throw new ArtifactMalformedException("Invalid forge installation configuration"); + args.add(parsed); + } + + command.addAll(args); + + LOG.info("Executing external processor " + processor.getJar().toString() + ", command line: " + new CommandBuilder().addAll(command).toString()); + int exitCode = SystemUtils.callExternalProcess(command); + if (exitCode != 0) + throw new IOException("Game processor exited abnormally with code " + exitCode); + + for (Map.Entry entry : outputs.entrySet()) { + Path artifact = Paths.get(entry.getKey()); + if (!Files.isRegularFile(artifact)) + throw new FileNotFoundException("File missing: " + artifact); + + String code; + try (InputStream stream = Files.newInputStream(artifact)) { + code = DigestUtils.digestToString("SHA-1", stream); + } + + if (!Objects.equals(code, entry.getValue())) { + Files.delete(artifact); + throw new ChecksumMismatchException("SHA-1", entry.getValue(), code); + } + } + } + } + + private final DefaultDependencyManager dependencyManager; + private final DefaultGameRepository gameRepository; + private final Version version; + private final Path installer; + private final List> dependents = new ArrayList<>(1); + private final List> dependencies = new ArrayList<>(1); + + private ForgeNewInstallProfile profile; + private List processors; + private Version neoForgeVersion; + private final String selfVersion; + + private Path tempDir; + private AtomicInteger processorDoneCount = new AtomicInteger(0); + + NeoForgeOldInstallTask(DefaultDependencyManager dependencyManager, Version version, String selfVersion, Path installer) { + this.dependencyManager = dependencyManager; + this.gameRepository = dependencyManager.getGameRepository(); + this.version = version; + this.installer = installer; + this.selfVersion = selfVersion; + + setSignificance(TaskSignificance.MAJOR); + } + + private static String replaceTokens(Map tokens, String value) { + StringBuilder buf = new StringBuilder(); + for (int x = 0; x < value.length(); x++) { + char c = value.charAt(x); + if (c == '\\') { + if (x == value.length() - 1) + throw new IllegalArgumentException("Illegal pattern (Bad escape): " + value); + buf.append(value.charAt(++x)); + } else if (c == '{' || c == '\'') { + StringBuilder key = new StringBuilder(); + for (int y = x + 1; y <= value.length(); y++) { + if (y == value.length()) + throw new IllegalArgumentException("Illegal pattern (Unclosed " + c + "): " + value); + char d = value.charAt(y); + if (d == '\\') { + if (y == value.length() - 1) + throw new IllegalArgumentException("Illegal pattern (Bad escape): " + value); + key.append(value.charAt(++y)); + } else { + if (c == '{' && d == '}') { + x = y; + break; + } + if (c == '\'' && d == '\'') { + x = y; + break; + } + key.append(d); + } + } + if (c == '\'') { + buf.append(key); + } else { + if (!tokens.containsKey(key.toString())) + throw new IllegalArgumentException("Illegal pattern: " + value + " Missing Key: " + key); + buf.append(tokens.get(key.toString())); + } + } else { + buf.append(c); + } + } + return buf.toString(); + } + + private String parseLiteral(String literal, Map var, ExceptionalFunction plainConverter) throws E { + if (StringUtils.isSurrounded(literal, "{", "}")) + return var.get(StringUtils.removeSurrounding(literal, "{", "}")); + else if (StringUtils.isSurrounded(literal, "'", "'")) + return StringUtils.removeSurrounding(literal, "'"); + else if (StringUtils.isSurrounded(literal, "[", "]")) + return gameRepository.getArtifactFile(version, Artifact.fromDescriptor(StringUtils.removeSurrounding(literal, "[", "]"))).toString(); + else + return plainConverter.apply(replaceTokens(var, literal)); + } + + private String parseLiteral(String literal, Map var) { + return parseLiteral(literal, var, ExceptionalFunction.identity()); + } + + @Override + public Collection> getDependents() { + return dependents; + } + + @Override + public Collection> getDependencies() { + return dependencies; + } + + @Override + public boolean doPreExecute() { + return true; + } + + @Override + public void preExecute() throws Exception { + try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) { + profile = JsonUtils.fromNonNullJson(FileUtils.readText(fs.getPath("install_profile.json")), ForgeNewInstallProfile.class); + processors = profile.getProcessors(); + neoForgeVersion = JsonUtils.fromNonNullJson(FileUtils.readText(fs.getPath(profile.getJson())), Version.class); + + for (Library library : profile.getLibraries()) { + Path file = fs.getPath("maven").resolve(library.getPath()); + if (Files.exists(file)) { + Path dest = gameRepository.getLibraryFile(version, library).toPath(); + FileUtils.copyFile(file, dest); + } + } + + if (profile.getPath().isPresent()) { + Path mainJar = profile.getPath().get().getPath(fs.getPath("maven")); + if (Files.exists(mainJar)) { + Path dest = gameRepository.getArtifactFile(version, profile.getPath().get()); + FileUtils.copyFile(mainJar, dest); + } + } + } catch (ZipException ex) { + throw new ArtifactMalformedException("Malformed forge installer file", ex); + } + + dependents.add(new GameLibrariesTask(dependencyManager, version, true, profile.getLibraries())); + } + + private Map parseOptions(List args, Map vars) { + Map options = new LinkedHashMap<>(); + String optionName = null; + for (String arg : args) { + if (arg.startsWith("--")) { + if (optionName != null) { + options.put(optionName, ""); + } + optionName = arg.substring(2); + } else { + if (optionName == null) { + // ignore + } else { + options.put(optionName, parseLiteral(arg, vars)); + optionName = null; + } + } + } + if (optionName != null) { + options.put(optionName, ""); + } + return options; + } + + private Task patchDownloadMojangMappingsTask(Processor processor, Map vars) { + Map options = parseOptions(processor.getArgs(), vars); + if (!"DOWNLOAD_MOJMAPS".equals(options.get("task")) || !"client".equals(options.get("side"))) + return null; + String version = options.get("version"); + String output = options.get("output"); + if (version == null || output == null) + return null; + + LOG.info("Patching DOWNLOAD_MOJMAPS task"); + return new VersionJsonDownloadTask(version, dependencyManager) + .thenComposeAsync(json -> { + DownloadInfo mappings = fromNonNullJson(json, Version.class) + .getDownloads().get(DownloadType.CLIENT_MAPPINGS); + if (mappings == null) { + throw new Exception("client_mappings download info not found"); + } + + List mappingsUrl = dependencyManager.getDownloadProvider() + .injectURLWithCandidates(mappings.getUrl()); + FileDownloadTask mappingsTask = new FileDownloadTask( + mappingsUrl, + new File(output), + IntegrityCheck.of("SHA-1", mappings.getSha1())); + mappingsTask.setCaching(true); + mappingsTask.setCacheRepository(dependencyManager.getCacheRepository()); + return mappingsTask; + }); + } + + private Task createProcessorTask(Processor processor, Map vars) { + Task task = patchDownloadMojangMappingsTask(processor, vars); + if (task == null) { + task = new ProcessorTask(processor, vars); + } + task.onDone().register( + () -> updateProgress(processorDoneCount.incrementAndGet(), processors.size())); + return task; + } + + @Override + public void execute() throws Exception { + tempDir = Files.createTempDirectory("neoforge_installer"); + + Map vars = new HashMap<>(); + + try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) { + for (Map.Entry entry : profile.getData().entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + + vars.put(key, parseLiteral(value, + Collections.emptyMap(), + str -> { + Path dest = Files.createTempFile(tempDir, null, null); + FileUtils.copyFile(fs.getPath(str), dest); + return dest.toString(); + })); + } + } catch (ZipException ex) { + throw new ArtifactMalformedException("Malformed neoforge installer file", ex); + } + + vars.put("SIDE", "client"); + vars.put("MINECRAFT_JAR", gameRepository.getVersionJar(version).getAbsolutePath()); + vars.put("MINECRAFT_VERSION", gameRepository.getVersionJar(version).getAbsolutePath()); + vars.put("ROOT", gameRepository.getBaseDirectory().getAbsolutePath()); + vars.put("INSTALLER", installer.toAbsolutePath().toString()); + vars.put("LIBRARY_DIR", gameRepository.getLibrariesDirectory(version).getAbsolutePath()); + + updateProgress(0, processors.size()); + + Task processorsTask = Task.runSequentially( + processors.stream() + .map(processor -> createProcessorTask(processor, vars)) + .toArray(Task[]::new)); + + dependencies.add( + processorsTask.thenComposeAsync( + dependencyManager.checkLibraryCompletionAsync(neoForgeVersion, true))); + + setResult(neoForgeVersion + .setPriority(30000) + .setId(LibraryAnalyzer.LibraryType.NEO_FORGE.getPatchId()) + .setVersion(selfVersion)); + } + + @Override + public boolean doPostExecute() { + return true; + } + + @Override + public void postExecute() throws Exception { + FileUtils.deleteDirectory(tempDir.toFile()); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeRemoteVersion.java new file mode 100644 index 000000000..3a08acedc --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeRemoteVersion.java @@ -0,0 +1,20 @@ +package org.jackhuang.hmcl.download.neoforge; + +import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.download.LibraryAnalyzer; +import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.task.Task; + +import java.util.List; + +public class NeoForgeRemoteVersion extends RemoteVersion { + public NeoForgeRemoteVersion(String gameVersion, String selfVersion, List urls) { + super(LibraryAnalyzer.LibraryType.NEO_FORGE.getPatchId(), gameVersion, selfVersion, null, urls); + } + + @Override + public Task getInstallTask(DefaultDependencyManager dependencyManager, Version baseVersion) { + return new NeoForgeInstallTask(dependencyManager, baseVersion, this); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaVersionConstraint.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaVersionConstraint.java index 53ec61242..c8f5a3799 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaVersionConstraint.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaVersionConstraint.java @@ -145,7 +145,6 @@ public enum JavaVersionConstraint { @Nullable JavaVersion javaVersion, @Nullable LibraryAnalyzer analyzer) { if (version == null || javaVersion == null || analyzer == null) return false; VersionNumber forgePatchVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.FORGE) - .map(LibraryAnalyzer.LibraryType.FORGE::patchVersion) .map(VersionNumber::asVersion) .orElse(null); if (forgePatchVersion == null) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModLoaderType.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModLoaderType.java index f4fcf4d39..97ec524b9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModLoaderType.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModLoaderType.java @@ -18,20 +18,11 @@ package org.jackhuang.hmcl.mod; public enum ModLoaderType { - UNKNOWN("Unknown"), - FORGE("Forge"), - FABRIC("Fabric"), - QUILT("Quilt"), - LITE_LOADER("LiteLoader"), - PACK("Pack"); - - private final String loaderName; - - ModLoaderType(String loaderName) { - this.loaderName = loaderName; - } - - public final String getLoaderName() { - return loaderName; - } + UNKNOWN, + FORGE, + NEO_FORGED, + FABRIC, + QUILT, + LITE_LOADER, + PACK; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java index 1fd22d38d..fa474943a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java @@ -588,6 +588,7 @@ public class CurseAddon implements RemoteMod.IMod { if ("fabric".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.FABRIC); else if ("forge".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.FORGE); else if ("quilt".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.QUILT); + else if ("neoforge".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.NEO_FORGED); else return Stream.empty(); }).collect(Collectors.toList()) ); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseInstallTask.java index a195a1120..6796cf330 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseInstallTask.java @@ -57,9 +57,9 @@ public final class CurseInstallTask extends Task { * Constructor. * * @param dependencyManager the dependency manager. - * @param zipFile the CurseForge modpack file. - * @param manifest The manifest content of given CurseForge modpack. - * @param name the new version name + * @param zipFile the CurseForge modpack file. + * @param manifest The manifest content of given CurseForge modpack. + * @param name the new version name * @see CurseManifest#readCurseForgeModpackManifest */ public CurseInstallTask(DefaultDependencyManager dependencyManager, File zipFile, Modpack modpack, CurseManifest manifest, String name) { @@ -81,6 +81,8 @@ public final class CurseInstallTask extends Task { builder.version("forge", modLoader.getId().substring("forge-".length())); } else if (modLoader.getId().startsWith("fabric-")) { builder.version("fabric", modLoader.getId().substring("fabric-".length())); + } else if (modLoader.getId().startsWith("neoforge-")) { + builder.version("neoforge", modLoader.getId().substring("neoforge-".length())); } } dependents.add(builder.buildAsync()); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java index b66b4729a..e0d9cc9a5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java @@ -513,7 +513,9 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository { loaders.stream().flatMap(loader -> { if ("fabric".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FABRIC); else if ("forge".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FORGE); + else if ("neoforge".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.NEO_FORGED); else if ("quilt".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.QUILT); + else if ("liteloader".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.LITE_LOADER); else return Stream.empty(); }).collect(Collectors.toList()) ));