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 2609468b9b..2faa26e382 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 @@ else if (LibraryAnalyzer.isModded(this, version)) { 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 c547ef37ca..13989d727a 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 @@ private static Task checkGameState(Profile profile, VersionSetting 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 d641472f1a..1a9c418691 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 d5c8a11f9f..7bc6a2b82e 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 InstallerItem(String id) { 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 static class InstallerItemGroup { 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 InstallerItemGroup() { 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, 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)); + }, 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 InstallerItemGroup() { 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 InstallerItemGroup() { } 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 1d12e0f17c..e9fd304379 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.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 void onRunning(Task task) { 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 StageNode(String stage) { 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 19235aa737..a36a2c09d0 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.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 InstallersPage(WizardController controller, HMCLGameRepository repository 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 @@ protected InstallersPageSkin(InstallersPage control) { { 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 9bee8d246b..e5b8311f6b 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.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 @@ else if (remoteVersion instanceof OptiFineRemoteVersion) 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 d968920b9e..2a8f8a74f0 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 @@ private static final class ModItem extends StackPane { 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 f3e66e86f6..b5ebc9865f 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 void loadVersion(Profile profile, String versionId) { 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 void loadVersion(Profile profile, String versionId) { 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 d31b5c9003..99f0d63ac2 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 ModInfoDialog extends JFXDialogLayout { 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 @@ protected void updateControl(ModInfoObject dataItem, boolean empty) { 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 01f26789c0..cde2d27f25 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 VersionIconDialog(Profile profile, String versionId, Runnable onFinish) { 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 0000000000..95c3b5cf69 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 0000000000..7d46d392e4 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 d826e81b43..8226870e78 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 f9a346feba..45c5e535b8 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 8c0305037c..95a2b0f984 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 69aeebda7b..bb7105900f 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.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 BMCLAPIDownloadProvider(String apiRoot) { 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 VersionList getVersionListById(String id) { return fabricApi; case "forge": return forge; + case "neoforge": + return neoforge; case "liteloader": return liteLoader; case "optifine": @@ -102,6 +107,7 @@ public String injectURL(String baseURL) { .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 a492c66c26..559f6b70fd 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.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 Task installLibraryAsync(Version oldVersion, Path installer) { 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 e1d78f0f2e..1d664484a1 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 @@ private Version removingMatchedLibrary(Version version, String libraryId) { /** * Remove library by library id - * @param libraryId patch id or "forge"/"optifine"/"liteloader"/"fabric"/"quilt" + * + * @param libraryId patch id or "forge"/"optifine"/"liteloader"/"fabric"/"quilt"/"neoforge" * @return this */ public LibraryAnalyzer removeLibrary(String libraryId) { @@ -139,7 +138,7 @@ public static LibraryAnalyzer analyze(Version version) { for (Library library : version.resolve(null).getLibraries()) { for (LibraryType type : LibraryType.values()) { if (type.matchLibrary(library)) { - libraries.put(type.getPatchId(), pair(library, type.patchVersion(library.getVersion()))); + libraries.put(type.getPatchId(), pair(library, type.patchVersion(version, library.getVersion()))); break; } } @@ -179,14 +178,56 @@ public enum LibraryType { private final Pattern FORGE_VERSION_MATCHER = Pattern.compile("^([0-9.]+)-(?[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 boolean matchLibrary(Library library) { return group.matcher(library.getGroupId()).matches() && artifact.matcher(library.getArtifactId()).matches(); } - public String patchVersion(String libraryVersion) { + public String patchVersion(Version gameVersion, String libraryVersion) { return libraryVersion; } } @@ -259,14 +300,14 @@ public String getLibraryVersion() { public static final String MOD_LAUNCHER_MAIN = "cpw.mods.modlauncher.Launcher"; public static final String BOOTSTRAP_LAUNCHER_MAIN = "cpw.mods.bootstraplauncher.BootstrapLauncher"; - public static final String[] FORGE_TWEAKERS = new String[] { - "net.minecraftforge.legacy._1_5_2.LibraryFixerTweaker", // 1.5.2 - "cpw.mods.fml.common.launcher.FMLTweaker", // 1.6.1 ~ 1.7.10 - "net.minecraftforge.fml.common.launcher.FMLTweaker" // 1.8 ~ 1.12.2 + public static final String[] FORGE_TWEAKERS = new String[]{ + "net.minecraftforge.legacy._1_5_2.LibraryFixerTweaker", // 1.5.2 + "cpw.mods.fml.common.launcher.FMLTweaker", // 1.6.1 ~ 1.7.10 + "net.minecraftforge.fml.common.launcher.FMLTweaker" // 1.8 ~ 1.12.2 }; - public static final String[] OPTIFINE_TWEAKERS = new String[] { - "optifine.OptiFineTweaker", - "optifine.OptiFineForgeTweaker" + public static final String[] OPTIFINE_TWEAKERS = new String[]{ + "optifine.OptiFineTweaker", + "optifine.OptiFineForgeTweaker" }; public static final String LITELOADER_TWEAKER = "com.mumfrey.liteloader.launch.LiteLoaderTweaker"; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MaintainTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MaintainTask.java index e8fe8a1e99..ced364ea55 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MaintainTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MaintainTask.java @@ -208,7 +208,7 @@ private static Version maintainGameWithCpwBoostrapLauncher(GameRepository reposi 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 34736b4c8d..33d11327e8 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.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 MojangDownloadProvider() { 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 VersionList getVersionListById(String id) { 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 7fc1584922..8c96ba321e 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 void execute() throws Exception { 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 0000000000..cf4cee35b8 --- /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 0000000000..58305192b5 --- /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 0000000000..3de56d9dfa --- /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 0000000000..3a08acedce --- /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 53ec612428..c8f5a37993 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 @@ protected boolean appliesToVersionImpl(VersionNumber gameVersionNumber, @Nullabl @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 f4fcf4d39a..97ec524b9f 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 1fd22d38de..fa474943a9 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 RemoteMod.Version toVersion() { 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 a195a11206..6796cf3308 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 CurseInstallTask(DefaultDependencyManager dependencyManager, File zipFile 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 b66b4729a2..e0d9cc9a56 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 Optional toVersion() { 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()) ));