Support Neoforge (#2548)

* Support #2376

* Add necessary @Nullable annotations

* Display different types of dependencies in different sections.

* Fix checkstyle

* Add I18N for different types of dependencies.

* Enhance UI

* Code cleanup

* Enhance UI

* Manually sort the result from curseforge when searching mods by name.

* Render the search results from remote mod repositories in several pages.

* Fix merge

* Fix

* Add a button which navigates to the modpack download page in the modpack installl page

* Fix I18N

* Render the mod loaders supported by the version in mod info page.

* Fix #2104

* Enhance TwoLineListItem

* Render the mod loader supported by this mod file on the ModListPage

* Fix chinese searching and curseforge searching

* Update I18N

* Fix

* Fix

* Select the specific game version when clicking the 'download' button on ModListPage

* Support HMCL to update mod_data and mod_pack data from https://github.com/huanghongxun/HMCL/raw/javafx/data-json/dynamic-remote-resources.json

* Enhance :HMCL:build.gradle.kts

* Revert parse_mcmod_data.py

* Abstract 'new Image' to FXUtils.newBuiltinImage and FXUtils.newRemoteImage

FXUtils.newBuiltinImage is used to load image which is supposed to be correct definitely and is a file within the jar. Or, it will throw ResourceNotFoundError.

FXUtils.newRemoteImage is used to load image from the internet. It will cache the data of images for the further usage. The cached data will be deleted when HMCL is closed or hidden.

* Add javadoc for FXUtils.newBuiltinImage and FXUtils.newRemoteImage.

* Fix checkstyle

* Fix

* Fix

* Fix

* Add license for RemoteResourceManager

* Remove TODO

* Enhance Chinese searching

* Support to decode metadata for local quilt mod.

* Enhance ModManager

* Fix checkstyle

* Refactor

* Fix

* Fix

* Refactor DownloadPage

* Fix

* Revert "Refactor DownloadPage"

This reverts commit 953558da77af5a0fe3153e77cdcb9b6affa30ffa.

* Refactor DownloadPage

* Refactor

* Fix

* Fix checkstyle

* Set org.jackhuang.hmcl.ui.construct.TwoLineListItem.TagChangeListener as a private static inner class.

* Fix

* Fix

* Fix

* Enhance SimpleMultimap

* Revert TwoLineListItem

* Fix

* Code cleanup

* Code cleanup

* Fix

* Code cleanup

* Add license for IModMetadataReader

* Add prefix 'Minecraft' at the supported minecrft version list in DownloadPage

* Fix #2498

* Update README_cn.md

* Opti ModMananger

* Log a warning message when 'hmcl.update_source.override' is used.

* Fix chinese searching

* Enhance chinese searching.

* Enhance memory usage

* Add basic support for NeoForge.

* Enable HMCl to show the version list of NeoForged.

* Fix checkstyle

* Enable HMCL to download and install NeoForged from BMCL. Fix some bugs.

* Close the mod version dialog window after clicking the downloading / save as button if the dependency list is empty.

* Fix

* Enable HMCL to install NeoForge from modpacks.

* Fix the dirty implememtation

* Fix UI

* Cache builtin images.

* Enhance FXUtils (Make tooltip installer faster).

* Fix

* Fix

* Fix #2560

* Fix typo

* Fix remote image cache.

* Fix javadoc

* Fix checkstyle

* Optimize FXUtils::shutdown

* Support NeoForged 1.20.2

* Fix merge

* I have no idea on why the sha1 was matched.

* Revert "Enhance FXUtils (Make tooltip installer faster)."

This reverts commit 0a49eb2c1204e4be7dc0df3084faa59fdf9b0394.

* Support multi download source in order balance the traffic of hmcl.huangyuhui.net and the download speed in China Mainland.

* Modify dynamic remote resource urls.

* Optimize codes with StringUtils.DynamicCommonSubsequence.

* Prevent unofficial HMCL to access HMCL Resource Update URL.

* Zip the dynamic-remote-resources json by Gradle automatically.

* Remove unnecessary getters.

* Fix

* Fix LibraryAnalyzer: Cannot analyze the version number of NeoForge correctly.

* Support to choose the NeoForge icon if a game is installed with NeoForge.

* Fix checkstyle

* Fix: The version number on DownloadPage of NeoForge is incorrect, which has leading string 'neoforge-'.

* Fix.

* Rename references. Fix: Invalid spaces in VersionIconType.

---------

Co-authored-by: Burning_TNT <pangyl08@163.com“>
This commit is contained in:
Burning_TNT 2024-01-08 20:35:03 +08:00 committed by GitHub
parent e6e4e36537
commit 4149876e04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 881 additions and 64 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -86,7 +86,7 @@ public class InstallerListPage extends ListPageBase<InstallerItem> 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<InstallerItem> 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);

View File

@ -335,15 +335,28 @@ class ModListPageSkin extends SkinBase<ModListPage> {
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<ModListPage> {
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());

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

View File

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

View File

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

View File

@ -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")

View File

@ -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) {

View File

@ -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<LibraryAnalyzer.LibraryMa
/**
* 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 final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
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 final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
private final Pattern FORGE_VERSION_MATCHER = Pattern.compile("^([0-9.]+)-(?<forge>[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<Arguments> optArgument = version.getArguments();
if (!optArgument.isPresent()) {
return null;
}
List<Argument> 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.LibraryMa
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 final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
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";
}

View File

@ -208,7 +208,7 @@ public class MaintainTask extends Task<Version> {
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<String> bslVersion = libraryAnalyzer.getVersion(BOOTSTRAP_LAUNCHER);

View File

@ -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 <a href="http://wiki.vg">http://wiki.vg</a>
* @author huangyuhui
* @see <a href="http://wiki.vg">http://wiki.vg</a>
*/
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":

View File

@ -192,7 +192,7 @@ public class ForgeNewInstallTask extends Task<Version> {
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;

View File

@ -0,0 +1,136 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.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<NeoForgeRemoteVersion> {
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).<List<NeoForgeVersion>>getJson(new TypeToken<List<NeoForgeVersion>>() {
}.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<NeoForgeRemoteVersion> 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.");
}
}
}
}

View File

@ -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<Version> {
private final DefaultDependencyManager dependencyManager;
private final Version version;
private final NeoForgeRemoteVersion remoteVersion;
private Path installer = null;
private FileDownloadTask dependent;
private Task<Version> 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<? extends Task<?>> getDependents() {
return Collections.singleton(dependent);
}
@Override
public Collection<? extends Task<?>> getDependencies() {
return Collections.singleton(dependency);
}
@Override
public void execute() throws Exception {
dependency = install(dependencyManager, version, installer);
}
public static Task<Version> install(DefaultDependencyManager dependencyManager, Version version, Path installer) throws IOException, VersionMismatchException {
Optional<String> 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", ""), "-");
}
}

View File

@ -0,0 +1,424 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.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<Version> {
private class ProcessorTask extends Task<Void> {
private final Processor processor;
private final Map<String, String> vars;
public ProcessorTask(@NotNull Processor processor, @NotNull Map<String, String> vars) {
this.processor = processor;
this.vars = vars;
setSignificance(TaskSignificance.MODERATE);
}
@Override
public void execute() throws Exception {
Map<String, String> outputs = new HashMap<>();
boolean miss = false;
for (Map.Entry<String, String> 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<String> command = new ArrayList<>();
command.add(JavaVersion.fromCurrentEnvironment().getBinary().toString());
command.add("-cp");
List<String> 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<String> 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<String, String> 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<Task<?>> dependents = new ArrayList<>(1);
private final List<Task<?>> dependencies = new ArrayList<>(1);
private ForgeNewInstallProfile profile;
private List<Processor> 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<String, String> 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 <E extends Exception> String parseLiteral(String literal, Map<String, String> var, ExceptionalFunction<String, String, E> 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<String, String> var) {
return parseLiteral(literal, var, ExceptionalFunction.identity());
}
@Override
public Collection<Task<?>> getDependents() {
return dependents;
}
@Override
public Collection<Task<?>> 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<String, String> parseOptions(List<String> args, Map<String, String> vars) {
Map<String, String> 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<String, String> vars) {
Map<String, String> 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<URL> 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<String, String> 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<String, String> vars = new HashMap<>();
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
for (Map.Entry<String, String> 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());
}
}

View File

@ -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<String> urls) {
super(LibraryAnalyzer.LibraryType.NEO_FORGE.getPatchId(), gameVersion, selfVersion, null, urls);
}
@Override
public Task<Version> getInstallTask(DefaultDependencyManager dependencyManager, Version baseVersion) {
return new NeoForgeInstallTask(dependencyManager, baseVersion, this);
}
}

View File

@ -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) {

View File

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

View File

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

View File

@ -57,9 +57,9 @@ public final class CurseInstallTask extends Task<Void> {
* 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<Void> {
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());

View File

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