diff --git a/.gitignore b/.gitignore index 0cff8dbffc7..82850c8a91c 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,8 @@ language-subtag-registry /HMCLBoot/build/ /minecraft/libraries/HMCLTransformerDiscoveryService/build/ /minecraft/libraries/HMCLMultiMCBootstrap/build/ +/minecraft/libraries/HMCLLegacyForgeHelper/build/ +/minecraft/libraries/HMCLModLoaderHelper/build/ /buildSrc/build/ # idea @@ -42,6 +44,8 @@ language-subtag-registry /HMCLCore/out/ /minecraft/libraries/HMCLTransformerDiscoveryService/out/ /minecraft/libraries/HMCLMultiMCBootstrap/out/ +/minecraft/libraries/HMCLLegacyForgeHelper/out/ +/minecraft/libraries/HMCLModLoaderHelper/out/ # eclipse /bin/ @@ -50,6 +54,8 @@ language-subtag-registry /HMCLCore/bin/ /minecraft/libraries/HMCLTransformerDiscoveryService/bin/ /minecraft/libraries/HMCLMultiMCBootstrap/bin/ +/minecraft/libraries/HMCLLegacyForgeHelper/bin/ +/minecraft/libraries/HMCLModLoaderHelper/bin/ .classpath .project .settings diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java index e13cd3d5d31..7b5566d1c2a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java @@ -31,8 +31,11 @@ import java.io.IOException; import java.io.InputStream; -import java.nio.file.*; -import java.util.*; +import java.nio.file.FileVisitOption; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Locale; +import java.util.Map; import java.util.stream.Stream; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -169,9 +172,27 @@ public void makeLaunchScript(Path scriptFile) throws IOException { protected void appendJvmArgs(CommandBuilder result) { super.appendJvmArgs(result); - if (options.isAllowAutoAgent() - && !options.isNoGeneratedJVMArgs() - && !options.isNoGeneratedOptimizingJVMArgs() + if (this.instance.getVersion().compareTo("1.6") < 0 && this.instance.hasComponent(GameComponentType.FORGE)) { + LOG.info("Attempting to patch game with legacy-forge-helper"); + try { + result.add("-javaagent:" + extractLegacyForgeHelper()); + } catch (Exception e) { + LOG.warning("Failed to extract legacy-forge-helper", e); + } + } + + if (this.instance.getVersion().compareTo("1.2") < 0 && this.instance.hasComponent(GameComponentType.FORGE)) { + LOG.info("Attempting to patch game with modloader-helper"); + try { + result.add("-javaagent:" + extractModLoaderHelper()); + } catch (Exception e) { + LOG.warning("Failed to extract modloader-helper", e); + } + } + + if (!options.isAllowAutoAgent() && !options.isNoGeneratedJVMArgs()) return; + + if (!options.isNoGeneratedOptimizingJVMArgs() && NativePatcher.needPatchMemoryUtil(manifest, options.getJava().getParsedVersion())) { LOG.info("Attempting to patch game with lwjgl-unsafe-agent"); try { @@ -220,4 +241,69 @@ private Path extractLwjglUnsafeAgent() throws IOException { return agentPath; } + private Path extractLegacyForgeHelper() throws IOException { + Library library = new Library(new Artifact("org.jackhuang.hmcl", "legacy-forge-helper", "1.0")); + String fileName = "HMCLLegacyForgeHelper-1.0.jar"; + + Path agentPath = instance.getLayout().getLibraryFile(instance.getId(), library).toAbsolutePath().normalize(); + if (agentPath.toString().contains("=")) { + throw new IOException("Invalid library path: " + agentPath); + } + + byte[] bytes; + try (InputStream input = DefaultLauncher.class.getResourceAsStream("/assets/game/" + fileName)) { + if (input == null) { + throw new IOException("/assets/game/" + fileName + " not found"); + } + + bytes = input.readAllBytes(); + } + + if (Files.isRegularFile(agentPath)) { + try { + if (Files.size(agentPath) == bytes.length) { + return agentPath; + } + } catch (IOException e) { + LOG.warning("Failed to check size of " + agentPath, e); + } + } + + Files.createDirectories(agentPath.getParent()); + FileUtils.saveSafely(agentPath, output -> output.write(bytes)); + return agentPath; + } + + private Path extractModLoaderHelper() throws IOException { + Library library = new Library(new Artifact("org.jackhuang.hmcl", "modloader-helper", "1.0")); + String fileName = "HMCLModLoaderHelper-1.0.jar"; + + Path agentPath = instance.getLayout().getLibraryFile(instance.getId(), library).toAbsolutePath().normalize(); + if (agentPath.toString().contains("=")) { + throw new IOException("Invalid library path: " + agentPath); + } + + byte[] bytes; + try (InputStream input = DefaultLauncher.class.getResourceAsStream("/assets/game/" + fileName)) { + if (input == null) { + throw new IOException("/assets/game/" + fileName + " not found"); + } + + bytes = input.readAllBytes(); + } + + if (Files.isRegularFile(agentPath)) { + try { + if (Files.size(agentPath) == bytes.length) { + return agentPath; + } + } catch (IOException e) { + LOG.warning("Failed to check size of " + agentPath, e); + } + } + + Files.createDirectories(agentPath.getParent()); + FileUtils.saveSafely(agentPath, output -> output.write(bytes)); + return agentPath; + } } 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 aaef6607bce..f1ee8d56f09 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -215,7 +215,7 @@ private void launch0() { }) ); }).withStage("launch.state.dependencies") - .thenComposeAsync(() -> new GameVerificationFixTask(gameInstance, gameInstance.getVersion(), launchManifest.get())) + .thenComposeAsync(() -> new GameVerificationFixTask(gameInstance, gameInstance.getVersion())) .thenComposeAsync(() -> { if (setting.getInheritable(GameSettings::allowAutoAgentProperty) || setting.getInheritable(GameSettings::noJVMOptionsProperty) 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 5e7c90e6b6d..19456a2ce90 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.cleanroom.CleanroomInstallTask; import org.jackhuang.hmcl.download.fabric.FabricAPIInstallTask; import org.jackhuang.hmcl.download.fabric.FabricInstallTask; +import org.jackhuang.hmcl.download.forge.ForgeLegacyInstallTask; import org.jackhuang.hmcl.download.forge.ForgeNewInstallTask; import org.jackhuang.hmcl.download.forge.ForgeOldInstallTask; import org.jackhuang.hmcl.download.game.GameAssetDownloadTask; @@ -172,7 +173,7 @@ public void onRunning(Task task) { task.setName(i18n("install.installer.install", i18n("install.installer.cleanroom"))); } else if (task instanceof LegacyFabricInstallTask) { task.setName(i18n("install.installer.install", i18n("install.installer.legacyfabric"))); - } else if (task instanceof ForgeNewInstallTask || task instanceof ForgeOldInstallTask) { + } else if (task instanceof ForgeNewInstallTask || task instanceof ForgeOldInstallTask || task instanceof ForgeLegacyInstallTask) { task.setName(i18n("install.installer.install", i18n("install.installer.forge"))); } else if (task instanceof NeoForgeInstallTask || task instanceof NeoForgeOldInstallTask) { task.setName(i18n("install.installer.install", i18n("install.installer.neoforge"))); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java index 43f63e07160..cfbbcd9be16 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java @@ -143,7 +143,7 @@ private void reloadCurrentInstance() { public void installOffline() { FileChooser chooser = new FileChooser(); - chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.modloader.installer"), "*.jar", "*.exe")); + chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.modloader.installer"), "*.jar", "*.exe", "*.zip")); Path file = Controllers.showOpenDialog(chooser); if (file != null) doInstallOffline(file); } diff --git a/HMCLCore/build.gradle.kts b/HMCLCore/build.gradle.kts index 8d108d24904..4061d4299d3 100644 --- a/HMCLCore/build.gradle.kts +++ b/HMCLCore/build.gradle.kts @@ -44,7 +44,9 @@ dependencies { tasks.processResources { listOf( "HMCLTransformerDiscoveryService", - "HMCLMultiMCBootstrap" + "HMCLMultiMCBootstrap", + "HMCLModLoaderHelper", + "HMCLLegacyForgeHelper" ).map { project(":$it").tasks["jar"] as Jar }.forEach { task -> dependsOn(task) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeOldModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeOldModMetadata.java index 81f358c94e6..1861385987c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeOldModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeOldModMetadata.java @@ -30,6 +30,7 @@ import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.tree.ZipFileTree; +import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.nio.file.Path; @@ -124,8 +125,12 @@ public String[] getAuthors() { return authors; } - public static LocalModFile fromFile(ModManager modManager, Path modFile, ZipFileTree tree) throws IOException, JsonParseException { - ZipArchiveEntry mcmod = tree.getEntry("mcmod.info"); + public static LocalModFile fromFile(@Nullable ModManager modManager, Path modFile, ZipFileTree tree) throws IOException, JsonParseException { + return fromFile(modManager, modFile, tree, "mcmod.info"); + } + + public static LocalModFile fromFile(@Nullable ModManager modManager, Path modFile, ZipFileTree tree, String infoFileName) throws IOException, JsonParseException { + ZipArchiveEntry mcmod = tree.getEntry(infoFileName); if (mcmod == null) throw new IOException("File " + modFile + " is not a Forge mod."); @@ -157,7 +162,7 @@ else if (firstToken == JsonToken.BEGIN_OBJECT) { authors = String.join(", ", metadata.getAuthorList()); if (StringUtils.isBlank(authors)) authors = metadata.getCredits(); - return new LocalModFile(modManager, modManager.getLocalMod(metadata.getModId(), ModLoaderType.FORGE), modFile, metadata.getName(), new LocalAddonFile.Description(metadata.getDescription()), + return new LocalModFile(modManager, modManager == null ? null : modManager.getLocalMod(metadata.getModId(), ModLoaderType.FORGE), modFile, metadata.getName(), new LocalAddonFile.Description(metadata.getDescription()), authors, metadata.getVersion(), metadata.getGameVersion(), StringUtils.isBlank(metadata.getUrl()) ? metadata.getUpdateUrl() : metadata.url, metadata.getLogoFile()); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/LocalModFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/LocalModFile.java index 32984fa3baa..85248076b10 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/LocalModFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/LocalModFile.java @@ -25,11 +25,15 @@ import org.jackhuang.hmcl.addon.RemoteAddonRepository; import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.util.io.FileUtils; +import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.*; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -40,7 +44,9 @@ public final class LocalModFile extends LocalAddonFile implements Comparable { private Path file; + @Nullable private final ModManager modManager; + @Nullable private final LocalMod mod; private final String name; private final Description description; @@ -52,11 +58,11 @@ public final class LocalModFile extends LocalAddonFile implements Comparable installComponentLocalAsync(GameInstance instan } try { - return ForgeInstallTask.install(this, baseManifest, gameVersion, installer); + return ForgeInstallation.install(this, baseManifest, gameVersion, installer); } catch (IOException ignore) { } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomInstallTask.java index 209809d93c7..4cdfba7f8ad 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomInstallTask.java @@ -186,13 +186,13 @@ public static Task install( } ForgeNewInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeNewInstallProfile.class); - if (!gameVersion.equals(profile.getMinecraft())) - throw new VersionMismatchException(profile.getMinecraft(), gameVersion); + if (!gameVersion.equals(profile.minecraft())) + throw new VersionMismatchException(profile.minecraft(), gameVersion); return new CleanroomInstallTask( dependencyManager, manifest, gameVersion, - modifyVersion(profile.getVersion()), + modifyVersion(profile.version()), installer); } else { throw new IOException(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeBMCLVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeBMCLVersionList.java index d9aae6a7c97..896dcfda083 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeBMCLVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeBMCLVersionList.java @@ -23,15 +23,16 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.gson.JsonSerializable; import org.jackhuang.hmcl.util.gson.Validation; import org.jackhuang.hmcl.util.io.NetworkUtils; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.time.Instant; import java.time.format.DateTimeParseException; import java.util.*; +import static org.jackhuang.hmcl.download.forge.ForgeInstallation.fromLookupVersion; +import static org.jackhuang.hmcl.download.forge.ForgeInstallation.toLookupVersion; import static org.jackhuang.hmcl.util.Lang.mapOf; import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf; @@ -57,14 +58,6 @@ public Task refreshAsync() { throw new UnsupportedOperationException("ForgeBMCLVersionList does not support loading the entire Forge remote version list."); } - private static String toLookupVersion(String gameVersion) { - return "1.7.10-pre4".equals(gameVersion) ? "1.7.10_pre4" : gameVersion; - } - - private static String fromLookupVersion(String lookupVersion) { - return "1.7.10_pre4".equals(lookupVersion) ? "1.7.10-pre4" : lookupVersion; - } - private static String toLookupBranch(String gameVersion, String branch) { if ("1.7.10-pre4".equals(gameVersion)) { return "prerelease"; @@ -86,21 +79,21 @@ public Task refreshAsync(String gameVersion) { if (version == null) continue; List urls = new ArrayList<>(); - for (ForgeVersion.File file : version.getFiles()) - if ("installer".equals(file.getCategory()) && "jar".equals(file.getFormat())) { - String branch = toLookupBranch(gameVersion, version.getBranch()); + for (ForgeVersion.File file : version.files()) + if (("installer".equals(file.category()) && "jar".equals(file.format())) || (("client".equals(file.category()) || "universal".equals(file.category())) && "zip".equals(file.format()))) { + String branch = toLookupBranch(gameVersion, version.branch()); - String classifier = lookupVersion + "-" + version.getVersion() + (branch.isEmpty() ? "" : '-' + branch); - String fileName1 = "forge-" + classifier + "-" + file.getCategory() + "." + file.getFormat(); - String fileName2 = "forge-" + classifier + "-" + lookupVersion + "-" + file.getCategory() + "." + file.getFormat(); + String classifier = lookupVersion + "-" + version.version() + (branch.isEmpty() ? "" : '-' + branch); + String fileName1 = "forge-" + classifier + "-" + file.category() + "." + file.format(); + String fileName2 = "forge-" + classifier + "-" + lookupVersion + "-" + file.category() + "." + file.format(); urls.add("https://files.minecraftforge.net/maven/net/minecraftforge/forge/" + classifier + "/" + fileName1); urls.add("https://files.minecraftforge.net/maven/net/minecraftforge/forge/" + classifier + "-" + lookupVersion + "/" + fileName2); urls.add(NetworkUtils.withQuery("https://bmclapi2.bangbang93.com/forge/download", mapOf( - pair("mcversion", version.getGameVersion()), - pair("version", version.getVersion()), + pair("mcversion", version.mcversion()), + pair("version", version.version()), pair("branch", branch), - pair("category", file.getCategory()), - pair("format", file.getFormat()) + pair("category", file.category()), + pair("format", file.format()) ))); } @@ -108,16 +101,16 @@ public Task refreshAsync(String gameVersion) { continue; Instant releaseDate = null; - if (version.getModified() != null) { + if (version.modified() != null) { try { - releaseDate = Instant.parse(version.getModified()); + releaseDate = Instant.parse(version.modified()); } catch (DateTimeParseException e) { - LOG.warning("Failed to parse instant " + version.getModified(), e); + LOG.warning("Failed to parse instant " + version.modified(), e); } } versions.put(gameVersion, new ForgeRemoteVersion( - fromLookupVersion(version.getGameVersion()), version.getVersion(), releaseDate, urls)); + fromLookupVersion(version.mcversion()), version.version(), releaseDate, urls)); } } finally { lock.writeLock().unlock(); @@ -132,14 +125,9 @@ public Optional getVersion(String gameVersion, String remote } @Immutable - public static final class ForgeVersion implements Validation { - - private final String branch; - private final int build; - private final String mcversion; - private final String modified; - private final String version; - private final List files; + @JsonSerializable + public record ForgeVersion(String branch, int build, String mcversion, String modified, String version, + List files) implements Validation { /** * No-arg constructor for Gson. @@ -149,44 +137,6 @@ public ForgeVersion() { this(null, 0, "", null, "", Collections.emptyList()); } - public ForgeVersion(String branch, int build, String mcversion, String modified, String version, List files) { - this.branch = branch; - this.build = build; - this.mcversion = mcversion; - this.modified = modified; - this.version = version; - this.files = files; - } - - @Nullable - public String getBranch() { - return branch; - } - - public int getBuild() { - return build; - } - - @NotNull - public String getGameVersion() { - return mcversion; - } - - @Nullable - public String getModified() { - return modified; - } - - @NotNull - public String getVersion() { - return version; - } - - @NotNull - public List getFiles() { - return files; - } - @Override public void validate() throws JsonParseException { if (files == null) @@ -198,32 +148,11 @@ public void validate() throws JsonParseException { } @Immutable - public static final class File { - private final String format; - private final String category; - private final String hash; - + @JsonSerializable + public record File(String format, String category, String hash) { public File() { this("", "", ""); } - - public File(String format, String category, String hash) { - this.format = format; - this.category = category; - this.hash = hash; - } - - public String getFormat() { - return format; - } - - public String getCategory() { - return category; - } - - public String getHash() { - return hash; - } } } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstall.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstall.java deleted file mode 100644 index a7a40607998..00000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstall.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2020 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.forge; - -import org.jackhuang.hmcl.game.Artifact; -import org.jackhuang.hmcl.util.Immutable; - -/** - * - * @author huangyuhui - */ -@Immutable -public final class ForgeInstall { - - private final String profileName; - private final String target; - private final Artifact path; - private final String version; - private final String filePath; - private final String welcome; - private final String minecraft; - private final String mirrorList; - private final String logo; - - public ForgeInstall() { - this(null, null, null, null, null, null, null, null, null); - } - - public ForgeInstall(String profileName, String target, Artifact path, String version, String filePath, String welcome, String minecraft, String mirrorList, String logo) { - this.profileName = profileName; - this.target = target; - this.path = path; - this.version = version; - this.filePath = filePath; - this.welcome = welcome; - this.minecraft = minecraft; - this.mirrorList = mirrorList; - this.logo = logo; - } - - public String getProfileName() { - return profileName; - } - - public String getTarget() { - return target; - } - - public Artifact getPath() { - return path; - } - - public String getVersion() { - return version; - } - - public String getFilePath() { - return filePath; - } - - public String getWelcome() { - return welcome; - } - - public String getMinecraft() { - return minecraft; - } - - public String getMirrorList() { - return mirrorList; - } - - public String getLogo() { - return logo; - } - -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallManifest.java new file mode 100644 index 00000000000..c89f231f064 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallManifest.java @@ -0,0 +1,32 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2020 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.forge; + +import org.jackhuang.hmcl.game.Artifact; +import org.jackhuang.hmcl.util.Immutable; +import org.jackhuang.hmcl.util.gson.JsonSerializable; + +/** + * + * @author huangyuhui + */ +@Immutable +@JsonSerializable +public record ForgeInstallManifest(String profileName, String target, Artifact path, String version, String filePath, + String welcome, String minecraft, String mirrorList, String logo) { +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java index 35e6b129d24..3ce2cddda72 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java @@ -17,30 +17,25 @@ */ package org.jackhuang.hmcl.download.forge; -import org.jackhuang.hmcl.download.*; +import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.download.UnsupportedInstallationException; +import org.jackhuang.hmcl.download.VersionMismatchException; import org.jackhuang.hmcl.download.game.GameDownloadTask; import org.jackhuang.hmcl.game.GameComponentAnalyzer; -import org.jackhuang.hmcl.game.GameComponentType; import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.game.GameInstancePatch; 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.versioning.GameVersionNumber; import java.io.IOException; -import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collection; import java.util.Collections; -import java.util.Map; -import static org.jackhuang.hmcl.download.UnsupportedInstallationException.CLEANROOM_NOT_COMPATIBLE_WITH_FORGE; import static org.jackhuang.hmcl.download.UnsupportedInstallationException.UNSUPPORTED_LAUNCH_WRAPPER; -import static org.jackhuang.hmcl.util.StringUtils.removePrefix; -import static org.jackhuang.hmcl.util.StringUtils.removeSuffix; +import static org.jackhuang.hmcl.download.forge.ForgeInstallation.detectForgeInstallerType; /** * @@ -75,9 +70,7 @@ public boolean doPreExecute() { public void preExecute() throws Exception { installer = Files.createTempFile("forge-installer", ".jar"); - dependent = new FileDownloadTask( - dependencyManager.getDownloadProvider().injectURLsWithCandidates(remote.getUrls()), - installer, null); + dependent = new FileDownloadTask(dependencyManager.getDownloadProvider().injectURLsWithCandidates(remote.getUrls()), installer, null); dependent.setCacheRepository(dependencyManager.getCacheRepository()); dependent.setCaching(true); dependent.addIntegrityCheckHandler(FileDownloadTask.ZIP_INTEGRITY_CHECK_HANDLER); @@ -107,114 +100,22 @@ public Collection> getDependencies() { @Override public void execute() throws IOException, VersionMismatchException, UnsupportedInstallationException { String originalMainClass = manifest.mainClass(); + if (GameVersionNumber.compare("1.13", remote.getGameVersion()) <= 0) { // Forge 1.13 is not compatible with fabric. if (!GameComponentAnalyzer.FORGE_OPTIFINE_MAIN.contains(originalMainClass)) throw new UnsupportedInstallationException(UNSUPPORTED_LAUNCH_WRAPPER); } - if (detectForgeInstallerType(remote.getGameVersion(), installer)) { - dependency = new GameDownloadTask(dependencyManager, manifest) - .thenComposeAsync(minecraftJar -> new ForgeNewInstallTask( - dependencyManager, - manifest, - minecraftJar, - remote.getSelfVersion(), - installer)); - } else { - dependency = new ForgeOldInstallTask(dependencyManager, manifest, remote.getSelfVersion(), installer); - } - } - - /// Returns whether a Forge installer uses the processor-based format. - /// - /// @param gameVersion Minecraft version expected by the installation - /// @param installer the Forge installer JAR - /// @return `true` for the processor-based format, or `false` for the legacy format - /// @throws IOException if the installer profile is missing, malformed, or - /// unsupported - /// @throws VersionMismatchException if the installer targets another Minecraft version - public static boolean detectForgeInstallerType(String gameVersion, Path installer) throws IOException, VersionMismatchException { - try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) { - String installProfileText = Files.readString(fs.getPath("install_profile.json")); - Map installProfile = JsonUtils.fromNonNullJson(installProfileText, Map.class); - if (installProfile.containsKey("spec")) { - ForgeNewInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeNewInstallProfile.class); - if (!gameVersion.equals(profile.getMinecraft())) - throw new VersionMismatchException(profile.getMinecraft(), gameVersion); - return true; - } else if (installProfile.containsKey("install") && installProfile.containsKey("versionInfo")) { - ForgeInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeInstallProfile.class); - if (!gameVersion.equals(profile.install().getMinecraft())) - throw new VersionMismatchException(profile.install().getMinecraft(), gameVersion); - return false; - } else { - throw new IOException(); - } - } - } + var type = detectForgeInstallerType(remote.getGameVersion(), installer); - /// Creates a task that installs Forge from a local installer JAR. - /// - /// Processor-based installers obtain a verified vanilla client JAR from shared cache storage; - /// neither format reads an instance JAR. - /// - /// @param dependencyManager repository-scoped download services - /// @param manifest working manifest receiving the Forge patch - /// @param gameVersion Minecraft version expected by the installation - /// @param installer the Forge installer JAR - /// @return the task producing the Forge patch - /// @throws IOException if the installer profile is missing, malformed, or - /// unsupported - /// @throws VersionMismatchException if the installer targets another Minecraft version - /// @throws UnsupportedInstallationException if the manifest already contains Cleanroom - public static Task install( - DefaultDependencyManager dependencyManager, - GameInstanceManifest manifest, - String gameVersion, - Path installer) throws IOException, VersionMismatchException, UnsupportedInstallationException { - try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) { - String installProfileText = Files.readString(fs.getPath("install_profile.json")); - Map installProfile = JsonUtils.fromNonNullJson(installProfileText, Map.class); - if (installProfile.containsKey("spec")) { - checkCleanroomCompatibility(manifest, gameVersion); - ForgeNewInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeNewInstallProfile.class); - if (!gameVersion.equals(profile.getMinecraft())) - throw new VersionMismatchException(profile.getMinecraft(), gameVersion); - return new GameDownloadTask(dependencyManager, manifest) - .thenComposeAsync(minecraftJar -> new ForgeNewInstallTask( - dependencyManager, - manifest, - minecraftJar, - modifyVersion(gameVersion, profile.getVersion()), - installer)); - } else if (installProfile.containsKey("install") && installProfile.containsKey("versionInfo")) { - checkCleanroomCompatibility(manifest, gameVersion); - ForgeInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeInstallProfile.class); - if (!gameVersion.equals(profile.install().getMinecraft())) - throw new VersionMismatchException(profile.install().getMinecraft(), gameVersion); - return new ForgeOldInstallTask(dependencyManager, manifest, modifyVersion(gameVersion, profile.install().getPath().getVersion().replaceAll("(?i)forge", "")), installer); - } else { - throw new IOException(); - } + switch (type) { + case LEGACY_MODLOADER, LEGACY_FML -> + dependency = new ForgeLegacyInstallTask(dependencyManager, manifest, remote.getSelfVersion(), installer, type); + case OLD -> + dependency = new ForgeOldInstallTask(dependencyManager, manifest, remote.getSelfVersion(), installer); + case NEW -> + dependency = new GameDownloadTask(dependencyManager, manifest).thenComposeAsync(minecraftJar -> new ForgeNewInstallTask(dependencyManager, manifest, minecraftJar, remote.getSelfVersion(), installer)); } } - - /// Rejects Forge installation when the manifest already contains Cleanroom. - /// - /// @param manifest working manifest receiving the Forge patch - /// @param gameVersion Minecraft version used for component analysis - /// @throws UnsupportedInstallationException if the manifest already contains Cleanroom - private static void checkCleanroomCompatibility( - GameInstanceManifest manifest, - String gameVersion) throws UnsupportedInstallationException { - GameComponentAnalyzer analyzer = GameComponentAnalyzer.analyze(manifest, GameVersionNumber.asGameVersion(gameVersion)); - if (analyzer.has(GameComponentType.CLEANROOM)) { - throw new UnsupportedInstallationException(CLEANROOM_NOT_COMPATIBLE_WITH_FORGE); - } - } - - private static String modifyVersion(String gameVersion, String version) { - return removePrefix(removeSuffix(removePrefix(removeSuffix(removePrefix(version.replace(gameVersion, "").trim(), "-"), "-"), "_"), "_"), "forge-"); - } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallation.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallation.java new file mode 100644 index 00000000000..955c7f60ba5 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallation.java @@ -0,0 +1,161 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 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.forge; + +import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.download.UnsupportedInstallationException; +import org.jackhuang.hmcl.download.VersionMismatchException; +import org.jackhuang.hmcl.download.game.GameDownloadTask; +import org.jackhuang.hmcl.game.GameComponentAnalyzer; +import org.jackhuang.hmcl.game.GameComponentType; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; +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.versioning.GameVersionNumber; + +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +import static org.jackhuang.hmcl.download.UnsupportedInstallationException.CLEANROOM_NOT_COMPATIBLE_WITH_FORGE; +import static org.jackhuang.hmcl.util.StringUtils.removePrefix; +import static org.jackhuang.hmcl.util.StringUtils.removeSuffix; + +public final class ForgeInstallation { + private ForgeInstallation() { + throw new AssertionError(); + } + + /// Returns whether a Forge installer uses the processor-based format. + /// + /// @param gameVersion Minecraft version expected by the installation + /// @param installer the Forge installer JAR + /// @return `true` for the processor-based format, or `false` for the legacy format + /// @throws IOException if the installer profile is missing, malformed, or + /// unsupported + /// @throws VersionMismatchException if the installer targets another Minecraft version + public static ForgeInstallerType detectForgeInstallerType(String gameVersion, Path installer) throws IOException, VersionMismatchException { + try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) { + if ((Files.isRegularFile(fs.getPath("fmlversion.properties")) || Files.isRegularFile(fs.getPath("forgeversion.properties"))) && !Files.isRegularFile(fs.getPath("install_profile.json"))) { + return ForgeInstallerType.LEGACY_FML; + } + + if ((Files.isRegularFile(fs.getPath("mod_MinecraftForge.class"))) && !Files.isRegularFile(fs.getPath("install_profile.json"))) { + return ForgeInstallerType.LEGACY_MODLOADER; + } + + String installProfileText = Files.readString(fs.getPath("install_profile.json")); + Map installProfile = JsonUtils.fromNonNullJson(installProfileText, Map.class); + if (installProfile.containsKey("spec")) { + ForgeNewInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeNewInstallProfile.class); + if (!gameVersion.equals(profile.minecraft())) + throw new VersionMismatchException(profile.minecraft(), gameVersion); + return ForgeInstallerType.NEW; + } else if (installProfile.containsKey("install") && installProfile.containsKey("versionInfo")) { + ForgeOldInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeOldInstallProfile.class); + if (!gameVersion.equals(profile.install().minecraft())) + throw new VersionMismatchException(profile.install().minecraft(), gameVersion); + return ForgeInstallerType.OLD; + } else { + throw new IOException(); + } + } + } + + /// Creates a task that installs Forge from a local installer JAR. + /// + /// Processor-based installers obtain a verified vanilla client JAR from shared cache storage; + /// neither format reads an instance JAR. + /// + /// @param dependencyManager repository-scoped download services + /// @param manifest working manifest receiving the Forge patch + /// @param gameVersion Minecraft version expected by the installation + /// @param installer the Forge installer JAR + /// @return the task producing the Forge patch + /// @throws IOException if the installer profile is missing, malformed, or + /// unsupported + /// @throws VersionMismatchException if the installer targets another Minecraft version + /// @throws UnsupportedInstallationException if the manifest already contains Cleanroom + public static Task install(DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, String gameVersion, Path installer) throws IOException, VersionMismatchException, UnsupportedInstallationException { + try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) { + var type = detectForgeInstallerType(gameVersion, installer); + + switch (type) { + case LEGACY_MODLOADER, LEGACY_FML -> { + var forgeProfile = ForgeLegacyInstallProfile.parse(installer); + return new ForgeLegacyInstallTask(dependencyManager, manifest, forgeProfile != null ? forgeProfile.forgeVersion() : null, installer, type); + } + case OLD -> { + checkCleanroomCompatibility(manifest, gameVersion); + String installProfileText = Files.readString(fs.getPath("install_profile.json")); + ForgeOldInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeOldInstallProfile.class); + + return new ForgeOldInstallTask(dependencyManager, manifest, modifyVersion(gameVersion, profile.install().path().getVersion().replaceAll("(?i)forge", "")), installer); + } + case NEW -> { + checkCleanroomCompatibility(manifest, gameVersion); + String installProfileText = Files.readString(fs.getPath("install_profile.json")); + ForgeNewInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeNewInstallProfile.class); + + return new GameDownloadTask(dependencyManager, manifest).thenComposeAsync(minecraftJar -> new ForgeNewInstallTask(dependencyManager, manifest, minecraftJar, modifyVersion(gameVersion, profile.version()), installer)); + } + default -> throw new IOException(); + } + } + } + + /// Rejects Forge installation when the manifest already contains Cleanroom. + /// + /// @param manifest working manifest receiving the Forge patch + /// @param gameVersion Minecraft version used for component analysis + /// @throws UnsupportedInstallationException if the manifest already contains Cleanroom + private static void checkCleanroomCompatibility( + GameInstanceManifest manifest, + String gameVersion) throws UnsupportedInstallationException { + GameComponentAnalyzer analyzer = GameComponentAnalyzer.analyze(manifest, GameVersionNumber.asGameVersion(gameVersion)); + + + if (analyzer.has(GameComponentType.CLEANROOM)) { + throw new UnsupportedInstallationException(CLEANROOM_NOT_COMPATIBLE_WITH_FORGE); + } + } + + private static String modifyVersion(String gameVersion, String version) { + return removePrefix(removeSuffix(removePrefix(removeSuffix(removePrefix(version.replace(gameVersion, "").trim(), "-"), "-"), "_"), "_"), "forge-"); + } + + public static String toLookupVersion(String gameVersion) { + return switch (gameVersion) { + case "1.7.10-pre4" -> "1.7.10_pre4"; + case "1.4" -> "1.4.0"; + default -> gameVersion; + }; + } + + public static String fromLookupVersion(String gameVersion) { + return switch (gameVersion) { + case "1.7.10_pre4" -> "1.7.10-pre4"; + case "1.4.0" -> "1.4"; + default -> gameVersion; + }; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallerType.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallerType.java new file mode 100644 index 00000000000..094948a3662 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallerType.java @@ -0,0 +1,25 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 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.forge; + +public enum ForgeInstallerType { + LEGACY_MODLOADER, + LEGACY_FML, + OLD, + NEW +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeLegacyInstallProfile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeLegacyInstallProfile.java new file mode 100644 index 00000000000..da60d46473b --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeLegacyInstallProfile.java @@ -0,0 +1,79 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 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.forge; + +import kala.compress.archivers.zip.ZipArchiveReader; +import org.jackhuang.hmcl.addon.meta.ForgeOldModMetadata; +import org.jackhuang.hmcl.addon.mod.LocalModFile; +import org.jackhuang.hmcl.util.io.CompressingUtils; +import org.jackhuang.hmcl.util.tree.ZipFileTree; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +public record ForgeLegacyInstallProfile(@Nullable String gameVersion, @Nullable String forgeVersion) { + @Nullable + public static ForgeLegacyInstallProfile parse(Path forgeArchive) { + try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(forgeArchive)) { + String gameVersion = null; + String forgeVersion = null; + + if (Files.isRegularFile(fs.getPath("fmlversion.properties"))) { + Properties properties = new Properties(); + properties.load(Files.newInputStream(fs.getPath("fmlversion.properties"))); + + gameVersion = properties.getProperty("fmlbuild.mcversion"); + } + + if (Files.isRegularFile(fs.getPath("forgeversion.properties"))) { + Properties properties = new Properties(); + properties.load(Files.newInputStream(fs.getPath("forgeversion.properties"))); + List list = new ArrayList<>(); + list.add(properties.getProperty("forge.major.number")); + list.add(properties.getProperty("forge.minor.number")); + list.add(properties.getProperty("forge.revision.number")); + list.add(properties.getProperty("forge.build.number")); + forgeVersion = String.join(".", list); + } + + if (forgeVersion == null && Files.isRegularFile(fs.getPath("mod_MinecraftForge.info"))) { + try (ZipFileTree tree = new ZipFileTree(new ZipArchiveReader(forgeArchive))) { + LocalModFile metadata = ForgeOldModMetadata.fromFile(null, forgeArchive, tree, "mod_MinecraftForge.info"); + forgeVersion = metadata.getVersion(); + } + } + + if (forgeVersion == null && gameVersion == null && !Files.isRegularFile(fs.getPath("mod_MinecraftForge.class"))) { + return null; + } + + return new ForgeLegacyInstallProfile(gameVersion, forgeVersion); + } catch (IOException ioException) { + LOG.warning("Failed to parse forge archive " + forgeArchive, ioException); + return null; + } + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeLegacyInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeLegacyInstallTask.java new file mode 100644 index 00000000000..5151f74efe7 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeLegacyInstallTask.java @@ -0,0 +1,138 @@ +/* + * 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.forge; + +import org.jackhuang.hmcl.download.ArtifactMalformedException; +import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.game.*; +import org.jackhuang.hmcl.task.FileDownloadTask; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.util.DigestUtils; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; +import java.util.zip.ZipFile; + +public final class ForgeLegacyInstallTask extends Task { + public static final Library MODLOADER_LIBRARY = new Library(new Artifact("modloader", "modloader", "1.1")); + public static final String MODLOADER_DOWNLOAD_URL = "https://hmcl.glavo.site/metadata/fmllibs/ModLoader%201.1.zip"; + public static final Library MODLOADER_MP_LIBRARY = new Library(new Artifact("modloader", "modloader-mp", "1.1")); + public static final String MODLOADER_MP_DOWNLOAD_URL = "https://hmcl.glavo.site/metadata/fmllibs/ModLoaderMP%201.1%20v4.zip"; + + private final DefaultDependencyManager dependencyManager; + private final GameInstanceManifest manifest; + private final Path installer; + private final @Nullable String selfVersion; + private final ForgeInstallerType type; + private final List> dependencies = new ArrayList<>(1); + + ForgeLegacyInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, @Nullable String selfVersion, Path installer, ForgeInstallerType type) throws IOException { + this.dependencyManager = dependencyManager; + this.manifest = manifest; + this.installer = installer; + if (selfVersion != null) + this.selfVersion = selfVersion; + else + this.selfVersion = DigestUtils.digestToString("SHA-1", installer); + this.type = type; + + setSignificance(TaskSignificance.MAJOR); + } + + @Override + public List> getDependencies() { + return dependencies; + } + + @Override + public void preExecute() throws Exception { + if (type == ForgeInstallerType.LEGACY_MODLOADER) { + GameRepository gameRepository = dependencyManager.getGameRepository(); + + Path modloaderFile = gameRepository.getLayout().getLibraryFile(manifest.id(), MODLOADER_LIBRARY); + var modloaderDownloadTask = new FileDownloadTask(MODLOADER_DOWNLOAD_URL, modloaderFile, null); + modloaderDownloadTask.setCacheRepository(dependencyManager.getCacheRepository()); + modloaderDownloadTask.setCaching(true); + modloaderDownloadTask.addIntegrityCheckHandler(FileDownloadTask.ZIP_INTEGRITY_CHECK_HANDLER); + dependencies.add(modloaderDownloadTask); + + Path modloaderMpFile = gameRepository.getLayout().getLibraryFile(manifest.id(), MODLOADER_MP_LIBRARY); + var modloaderMpDownloadTask = new FileDownloadTask(MODLOADER_MP_DOWNLOAD_URL, modloaderMpFile, null); + modloaderMpDownloadTask.setCacheRepository(dependencyManager.getCacheRepository()); + modloaderMpDownloadTask.setCaching(true); + modloaderMpDownloadTask.addIntegrityCheckHandler(FileDownloadTask.ZIP_INTEGRITY_CHECK_HANDLER); + dependencies.add(modloaderMpDownloadTask); + } + } + + @Override + public boolean doPreExecute() { + return true; + } + + @Override + public void execute() throws Exception { + try (ZipFile zipFile = new ZipFile(installer.toFile())) { + ZipEntry entry1 = zipFile.getEntry("fmlversion.properties"); + ZipEntry entry2 = zipFile.getEntry("mod_MinecraftForge.class"); + + InputStream stream = null; + InputStream stream2 = null; + + if (entry1 != null) { + stream = zipFile.getInputStream(entry1); + } + if (entry2 != null) { + stream2 = zipFile.getInputStream(entry2); + } + + if (stream == null && stream2 == null) { + throw new ArtifactMalformedException("Malformed forge installer file, forgeversion.properties and mod_MinecraftForge.class both does not exist."); + } + + Library forgeLibrary = new Library(new Artifact("net.minecraftforge", "forge", selfVersion)); + GameRepository gameRepository = dependencyManager.getGameRepository(); + Path forgeFile = gameRepository.getLayout().getLibraryFile(manifest.id(), forgeLibrary); + Files.createDirectories(forgeFile.getParent()); + + try (InputStream is = Files.newInputStream(installer); OutputStream os = Files.newOutputStream(forgeFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { + is.transferTo(os); + } + + List libraries; + if (type == ForgeInstallerType.LEGACY_MODLOADER) { + libraries = List.of(forgeLibrary, MODLOADER_LIBRARY, MODLOADER_MP_LIBRARY); + } else { + libraries = List.of(forgeLibrary); + } + + setResult(new GameInstancePatch(GameComponentType.FORGE.getPatchId(), selfVersion, GameInstancePatch.PRIORITY_LOADER, null, null, libraries)); + } catch (ZipException ex) { + throw new ArtifactMalformedException("Malformed forge installer file", ex); + } + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallProfile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallProfile.java index d388403cd68..85c01b3fd6e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallProfile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallProfile.java @@ -29,47 +29,33 @@ import java.util.stream.Collectors; @Immutable -public class ForgeNewInstallProfile implements Validation { - - private final int spec; - private final String minecraft; - private final String json; - private final String version; - private final Artifact path; - private final List libraries; - private final List processors; - private final Map data; - - public ForgeNewInstallProfile(int spec, String minecraft, String json, String version, Artifact path, List libraries, List processors, Map data) { - this.spec = spec; - this.minecraft = minecraft; - this.json = json; - this.version = version; - this.path = path; - this.libraries = libraries; - this.processors = processors; - this.data = data; - } +public record ForgeNewInstallProfile(int spec, String minecraft, String json, String version, Artifact path, + List libraries, List processors, + Map data) implements Validation { /** * Specification for install_profile.json. */ - public int getSpec() { + @Override + public int spec() { return spec; } /** * Vanilla game version that this installer supports. */ - public String getMinecraft() { + @Override + public String minecraft() { return minecraft; } /** * Version json to be installed. + * * @return path of the version json relative to the installer JAR file. */ - public String getJson() { + @Override + public String json() { return json; } @@ -77,12 +63,14 @@ public String getJson() { * * @return forge version. */ - public String getVersion() { + @Override + public String version() { return version; } /** * Maven artifact path for the main jar to install. + * * @return artifact path of the main jar. */ public Optional getPath() { @@ -91,16 +79,19 @@ public Optional getPath() { /** * Libraries that processors depend on. + * * @return the required dependencies. */ - public List getLibraries() { + @Override + public List libraries() { return libraries == null ? Collections.emptyList() : libraries; } /** * Tasks to be executed to setup modded environment. */ - public List getProcessors() { + @Override + public List processors() { if (processors == null) return Collections.emptyList(); return processors.stream().filter(p -> p.isSide("client")).collect(Collectors.toList()); } @@ -114,7 +105,7 @@ public Map getData() { if (data == null) return new HashMap<>(); - return data.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getClient())); + return data.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().client())); } @Override @@ -123,23 +114,12 @@ public void validate() throws JsonParseException, TolerableValidationException { throw new JsonParseException("ForgeNewInstallProfile is malformed"); } - public static class Processor implements Validation { - private final List sides; - private final Artifact jar; - private final List classpath; - private final List args; - private final Map outputs; - - public Processor(List sides, Artifact jar, List classpath, List args, Map outputs) { - this.sides = sides; - this.jar = jar; - this.classpath = classpath; - this.args = args; - this.outputs = outputs; - } + public record Processor(List sides, Artifact jar, List classpath, List args, + Map outputs) implements Validation { /** * Check which side this processor should be run on. We only support client install currently. + * * @param side can be one of "client", "server", "extract". * @return true if the processor can run on the side. */ @@ -149,17 +129,21 @@ public boolean isSide(String side) { /** * The executable jar of this processor task. Will be executed in installation process. + * * @return the artifact path of executable jar. */ - public Artifact getJar() { + @Override + public Artifact jar() { return jar; } /** * The dependencies of this processor task. + * * @return the artifact path of dependencies. */ - public List getClasspath() { + @Override + public List classpath() { return classpath == null ? Collections.emptyList() : classpath; } @@ -170,10 +154,12 @@ public List getClasspath() { * {entry}: Get corresponding value of the entry in {@link ForgeNewInstallProfile#getData()} * {MINECRAFT_JAR}: path of the Minecraft jar. * {SIDE}: values other than "client" will be ignored. + * * @return arguments to pass to the processor jar. * @see ForgeNewInstallTask#parseLiteral(String, Map, ExceptionalFunction) */ - public List getArgs() { + @Override + public List args() { return args == null ? Collections.emptyList() : args; } @@ -182,10 +168,12 @@ public List getArgs() { * Arguments to pass to the processor jar. * Keys can be in one of [artifact] or {entry}. Should be file path. * Values can be in one of {entry} or 'literal'. Should be SHA-1 checksum. + * * @return files output from this processor. * @see ForgeNewInstallTask#parseLiteral(String, Map, ExceptionalFunction) */ - public Map getOutputs() { + @Override + public Map outputs() { return outputs == null ? Collections.emptyMap() : outputs; } @@ -196,21 +184,18 @@ public void validate() throws JsonParseException, TolerableValidationException { } } - public static class Datum { - private final String client; - - public Datum(String client) { - this.client = client; - } + public record Datum(String client) { /** * Can be in the following formats: * [value]: An artifact path. * 'value': A string literal. * value: A file in the installer package, to be extracted to a temp folder, and then have the absolute path in replacements. + * * @return Value to use for the client install */ - public String getClient() { + @Override + public String client() { return client; } } 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 2d5d60b41fe..b8e6fac7ff3 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 @@ -20,9 +20,10 @@ import org.jackhuang.hmcl.download.ArtifactMalformedException; import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.forge.ForgeNewInstallProfile.Processor; -import org.jackhuang.hmcl.download.game.GameLibrariesTask; import org.jackhuang.hmcl.download.game.GameInstanceJsonDownloadTask; +import org.jackhuang.hmcl.download.game.GameLibrariesTask; import org.jackhuang.hmcl.game.*; +import org.jackhuang.hmcl.java.JavaRuntime; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.DigestUtils; @@ -34,7 +35,6 @@ 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.java.JavaRuntime; import org.jackhuang.hmcl.util.platform.SystemUtils; import org.jetbrains.annotations.NotNull; @@ -53,15 +53,15 @@ import java.util.jar.JarFile; import java.util.zip.ZipException; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.gson.JsonUtils.fromNonNullJson; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; public class ForgeNewInstallTask extends Task { private class ProcessorTask extends Task { - private Processor processor; - private Map vars; + private final Processor processor; + private final Map vars; public ProcessorTask(@NotNull Processor processor, @NotNull Map vars) { this.processor = processor; @@ -74,7 +74,7 @@ public void execute() throws Exception { Map outputs = new HashMap<>(); boolean miss = false; - for (Map.Entry entry : processor.getOutputs().entrySet()) { + for (Map.Entry entry : processor.outputs().entrySet()) { String key = entry.getKey(); String value = entry.getValue(); @@ -105,11 +105,11 @@ public void execute() throws Exception { } } - if (!processor.getOutputs().isEmpty() && !miss) { + if (!processor.outputs().isEmpty() && !miss) { return; } - Path jar = gameRepository.getLayout().getArtifactFile(processor.getJar()); + Path jar = gameRepository.getLayout().getArtifactFile(processor.jar()); if (!Files.isRegularFile(jar)) throw new FileNotFoundException("Game processor file not found, should be downloaded in preprocess"); @@ -125,8 +125,8 @@ public void execute() throws Exception { command.add(JavaRuntime.getDefault().getBinary().toString()); command.add("-cp"); - List classpath = new ArrayList<>(processor.getClasspath().size() + 1); - for (Artifact artifact : processor.getClasspath()) { + List classpath = new ArrayList<>(processor.classpath().size() + 1); + for (Artifact artifact : processor.classpath()) { Path file = gameRepository.getLayout().getArtifactFile(artifact); if (!Files.isRegularFile(file)) throw new Exception("Game processor dependency missing"); @@ -137,8 +137,8 @@ public void execute() throws Exception { command.add(mainClass); - List args = new ArrayList<>(processor.getArgs().size()); - for (String arg : processor.getArgs()) { + List args = new ArrayList<>(processor.args().size()); + for (String arg : processor.args()) { String parsed = parseLiteral(arg, vars); if (parsed == null) throw new ArtifactMalformedException("Invalid forge installation configuration"); @@ -147,7 +147,7 @@ public void execute() throws Exception { command.addAll(args); - LOG.info("Executing external processor " + processor.getJar().toString() + ", command line: " + new CommandBuilder().addAll(command).toString()); + LOG.info("Executing external processor " + processor.jar().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); @@ -304,10 +304,10 @@ public boolean doPreExecute() { public void preExecute() throws Exception { try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) { profile = JsonUtils.fromNonNullJson(Files.readString(fs.getPath("install_profile.json")), ForgeNewInstallProfile.class); - processors = profile.getProcessors(); - forgeVersion = JsonUtils.fromNonNullJson(Files.readString(fs.getPath(profile.getJson())), GameInstanceManifest.class); + processors = profile.processors(); + forgeVersion = JsonUtils.fromNonNullJson(Files.readString(fs.getPath(profile.json())), GameInstanceManifest.class); - for (Library library : profile.getLibraries()) { + for (Library library : profile.libraries()) { Path file = fs.getPath("maven").resolve(library.getPath()); if (Files.exists(file)) { Path dest = gameRepository.getLayout().getLibraryFile(manifest.id(), library); @@ -326,7 +326,7 @@ public void preExecute() throws Exception { throw new ArtifactMalformedException("Malformed forge installer file", ex); } - dependents.add(new GameLibrariesTask(dependencyManager, manifest, true, profile.getLibraries())); + dependents.add(new GameLibrariesTask(dependencyManager, manifest, true, profile.libraries())); } private Map parseOptions(List args, Map vars) { @@ -354,7 +354,7 @@ private Map parseOptions(List args, Map } private Task patchDownloadMojangMappingsTask(Processor processor, Map vars) { - Map options = parseOptions(processor.getArgs(), vars); + Map options = parseOptions(processor.args(), vars); if (!"DOWNLOAD_MOJMAPS".equals(options.get("task")) || !"client".equals(options.get("side"))) return null; String version = options.get("version"); @@ -424,7 +424,7 @@ public void execute() throws Exception { vars.put("SIDE", "client"); vars.put("MINECRAFT_JAR", FileUtils.getAbsolutePath(isolatedMinecraftJar)); - vars.put("MINECRAFT_VERSION", profile.getMinecraft()); + vars.put("MINECRAFT_VERSION", profile.minecraft()); vars.put("ROOT", FileUtils.getAbsolutePath(gameRepository.getBaseDirectory())); vars.put("INSTALLER", installer.toAbsolutePath().toString()); vars.put("LIBRARY_DIR", FileUtils.getAbsolutePath(gameRepository.getLayout().getLibrariesDirectory())); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallProfile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallProfile.java similarity index 83% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallProfile.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallProfile.java index 25482181990..5407f28d8e3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallProfile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallProfile.java @@ -27,16 +27,16 @@ /// @author huangyuhui @NotNullByDefault @JsonSerializable -public record ForgeInstallProfile(@SerializedName("install") ForgeInstall install, - @SerializedName("versionInfo") GameInstanceManifest versionInfo) { +public record ForgeOldInstallProfile(@SerializedName("install") ForgeInstallManifest install, + @SerializedName("versionInfo") GameInstanceManifest versionInfo) { - public ForgeInstallProfile { + public ForgeOldInstallProfile { Objects.requireNonNull(install, "install"); Objects.requireNonNull(versionInfo, "versionInfo"); } @Override - public ForgeInstall install() { + public ForgeInstallManifest install() { return install; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallTask.java index 6084d457e81..3c5f5ff3690 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallTask.java @@ -19,11 +19,7 @@ import org.jackhuang.hmcl.download.ArtifactMalformedException; import org.jackhuang.hmcl.download.DefaultDependencyManager; -import org.jackhuang.hmcl.game.GameComponentType; -import org.jackhuang.hmcl.game.GameInstanceManifest; -import org.jackhuang.hmcl.game.GameInstancePatch; -import org.jackhuang.hmcl.game.GameRepository; -import org.jackhuang.hmcl.game.Library; +import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.gson.JsonUtils; @@ -38,7 +34,7 @@ import java.util.zip.ZipException; import java.util.zip.ZipFile; -public class ForgeOldInstallTask extends Task { +public final class ForgeOldInstallTask extends Task { private final DefaultDependencyManager dependencyManager; private final GameInstanceManifest manifest; @@ -71,15 +67,15 @@ public void execute() throws Exception { InputStream stream = zipFile.getInputStream(zipFile.getEntry("install_profile.json")); if (stream == null) throw new ArtifactMalformedException("Malformed forge installer file, install_profile.json does not exist."); - ForgeInstallProfile installProfile = JsonUtils.fromNonNullJsonFully(stream, ForgeInstallProfile.class); + ForgeOldInstallProfile installProfile = JsonUtils.fromNonNullJsonFully(stream, ForgeOldInstallProfile.class); // unpack the universal jar in the installer file. - Library forgeLibrary = new Library(installProfile.install().getPath()); + Library forgeLibrary = new Library(installProfile.install().path()); GameRepository gameRepository = dependencyManager.getGameRepository(); Path forgeFile = gameRepository.getLayout().getLibraryFile(manifest.id(), forgeLibrary); Files.createDirectories(forgeFile.getParent()); - ZipEntry forgeEntry = zipFile.getEntry(installProfile.install().getFilePath()); + ZipEntry forgeEntry = zipFile.getEntry(installProfile.install().filePath()); try (InputStream is = zipFile.getInputStream(forgeEntry); OutputStream os = Files.newOutputStream(forgeFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { is.transferTo(os); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersion.java index a7bb771935f..f037f049da3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersion.java @@ -26,61 +26,8 @@ * @author huangyuhui */ @Immutable -public final class ForgeVersion implements Validation { - - private final String branch; - private final String mcversion; - private final String jobver; - private final String version; - private final int build; - private final long modified; - private final String[][] files; - - /** - * No-arg constructor for Gson. - */ - @SuppressWarnings("unused") - public ForgeVersion() { - this(null, null, null, null, 0, 0, null); - } - - public ForgeVersion(String branch, String mcversion, String jobver, String version, int build, long modified, String[][] files) { - this.branch = branch; - this.mcversion = mcversion; - this.jobver = jobver; - this.version = version; - this.build = build; - this.modified = modified; - this.files = files; - } - - public String getBranch() { - return branch; - } - - public String getGameVersion() { - return mcversion; - } - - public String getJobver() { - return jobver; - } - - public String getVersion() { - return version; - } - - public int getBuild() { - return build; - } - - public long getModified() { - return modified; - } - - public String[][] getFiles() { - return files; - } +public record ForgeVersion(String branch, String mcversion, String jobver, String version, int build, long modified, + String[][] files) implements Validation { @Override public void validate() throws JsonParseException { @@ -91,5 +38,4 @@ public void validate() throws JsonParseException { if (mcversion == null) throw new JsonParseException("ForgeVersion mcversion cannot be null"); } - } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersionList.java index be63c853989..4cbe5e0f2fa 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersionList.java @@ -29,6 +29,9 @@ import java.util.Collections; import java.util.Map; +import static org.jackhuang.hmcl.download.forge.ForgeInstallation.fromLookupVersion; +import static org.jackhuang.hmcl.download.forge.ForgeInstallation.toLookupVersion; + /** * * @author huangyuhui @@ -45,14 +48,6 @@ public boolean hasType() { return false; } - private static String toLookupVersion(String gameVersion) { - return "1.7.10-pre4".equals(gameVersion) ? "1.7.10_pre4" : gameVersion; - } - - private static String fromLookupVersion(String lookupVersion) { - return "1.7.10_pre4".equals(lookupVersion) ? "1.7.10-pre4" : lookupVersion; - } - @Override public Task refreshAsync() { return new GetTask(FORGE_LIST).thenGetJsonAsync(ForgeVersionRoot.class) @@ -64,29 +59,29 @@ public Task refreshAsync() { return; versions.clear(); - for (Map.Entry entry : root.getGameVersions().entrySet()) { + for (Map.Entry entry : root.mcversion().entrySet()) { String gameVersion = fromLookupVersion(VersionNumber.normalize(entry.getKey())); for (int v : entry.getValue()) { - ForgeVersion version = root.getNumber().get(v); + ForgeVersion version = root.number().get(v); if (version == null) continue; - String jar = null; - for (String[] file : version.getFiles()) - if (file.length > 1 && "installer".equals(file[1])) { - String classifier = version.getGameVersion() + "-" + version.getVersion() - + (StringUtils.isNotBlank(version.getBranch()) ? "-" + version.getBranch() : ""); - String fileName = root.getArtifact() + "-" + classifier + "-" + file[1] + "." + file[0]; - jar = root.getWebPath() + classifier + "/" + fileName; + String installer = null; + for (String[] file : version.files()) + if (file.length > 1) { + String classifier = version.mcversion() + "-" + version.version() + + (StringUtils.isNotBlank(version.branch()) ? "-" + version.branch() : ""); + String fileName = root.artifact() + "-" + classifier + "-" + file[1] + "." + file[0]; + installer = root.webpath() + classifier + "/" + fileName; } - if (jar == null) + if (installer == null) continue; versions.put(gameVersion, new ForgeRemoteVersion( - toLookupVersion(version.getGameVersion()), - version.getVersion(), - version.getModified() > 0 ? Instant.ofEpochSecond(version.getModified()) : null, - Collections.singletonList(jar) + toLookupVersion(version.mcversion()), + version.version(), + version.modified() > 0 ? Instant.ofEpochSecond(version.modified()) : null, + Collections.singletonList(installer) )); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersionRoot.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersionRoot.java index cad542569b9..cf294625bb1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersionRoot.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersionRoot.java @@ -28,74 +28,9 @@ * @author huangyuhui */ @Immutable -public final class ForgeVersionRoot implements Validation { - - private final String artifact; - private final String webpath; - private final String adfly; - private final String homepage; - private final String name; - private final Map branches; - private final Map mcversion; - private final Map promos; - private final Map number; - - /** - * No-arg constructor for Gson. - */ - @SuppressWarnings("unused") - public ForgeVersionRoot() { - this(null, null, null, null, null, null, null, null, null); - } - - public ForgeVersionRoot(String artifact, String webpath, String adfly, String homepage, String name, Map branches, Map mcversion, Map promos, Map number) { - this.artifact = artifact; - this.webpath = webpath; - this.adfly = adfly; - this.homepage = homepage; - this.name = name; - this.branches = branches; - this.mcversion = mcversion; - this.promos = promos; - this.number = number; - } - - public String getArtifact() { - return artifact; - } - - public String getWebPath() { - return webpath; - } - - public String getAdfly() { - return adfly; - } - - public String getHomePage() { - return homepage; - } - - public String getName() { - return name; - } - - public Map getBranches() { - return branches; - } - - public Map getGameVersions() { - return mcversion; - } - - public Map getPromos() { - return promos; - } - - public Map getNumber() { - return number; - } - +public record ForgeVersionRoot(String artifact, String webpath, String adfly, String homepage, String name, + Map branches, Map mcversion, Map promos, + Map number) implements Validation { @Override public void validate() throws JsonParseException { if (number == null) @@ -103,5 +38,4 @@ public void validate() throws JsonParseException { if (mcversion == null) throw new JsonParseException("ForgeVersionRoot mcversion cannot be null"); } - } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameLibrariesTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameLibrariesTask.java index fcd6e3ea716..0b80f16874f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameLibrariesTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameLibrariesTask.java @@ -18,10 +18,10 @@ package org.jackhuang.hmcl.download.game; import org.jackhuang.hmcl.download.AbstractDependencyManager; +import org.jackhuang.hmcl.download.forge.ForgeLegacyInstallTask; import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.util.DigestUtils; import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; @@ -30,7 +30,6 @@ import java.io.IOException; import java.io.InputStream; -import java.net.URI; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; @@ -119,48 +118,18 @@ public static boolean shouldDownloadLibrary(GameRepository gameRepository, GameI return false; } - private static boolean shouldDownloadFMLLib(FMLLib fmlLib, Path file) { - if (!Files.isRegularFile(file)) - return true; - - try { - return !DigestUtils.digestToString("SHA-1", file).equalsIgnoreCase(fmlLib.sha1); - } catch (IOException e) { - LOG.warning("Unable to calc hash value of file " + file, e); - return true; - } - } - /// {@inheritDoc} @Override public void execute() throws IOException { int progress = 0; GameRepository gameRepository = dependencyManager.getGameRepository(); for (Library library : libraries) { + boolean handled = false; + if (!library.appliesToCurrentEnvironment()) { continue; } - // https://github.com/HMCL-dev/HMCL/issues/3975 - if (library.is("net.minecraftforge", "minecraftforge") - && gameRepository instanceof DefaultGameRepository defaultGameRepository) { - List fmlLibs = getFMLLibs(library.version()); - if (fmlLibs != null) { - Path libDir = defaultGameRepository.getBaseDirectory().resolve("lib") - .toAbsolutePath().normalize(); - - for (FMLLib fmlLib : fmlLibs) { - Path file = libDir.resolve(fmlLib.name); - if (shouldDownloadFMLLib(fmlLib, file)) { - List uris = dependencyManager.getDownloadProvider() - .injectURLWithCandidates(fmlLib.downloadUrl()); - dependencies.add(new FileDownloadTask(uris, file) - .withCounter("hmcl.install.libraries")); - } - } - } - } - Path file = gameRepository.getLayout().getLibraryFile(manifest.id(), library); if ("optifine".equals(library.groupId()) && Files.exists(file)) { if (Files.exists(file) && libraries.stream().filter(it -> it.is("optifine", "OptiFine")) @@ -183,6 +152,7 @@ public void execute() throws IOException { "Bundled HMCLMultiMCBootstrap is missing.")) { Files.createDirectories(file.getParent()); Files.copy(input, file, StandardCopyOption.REPLACE_EXISTING); + handled = true; } } } else if (library.is("org.jackhuang.hmcl", "transformer-discovery-service")) { @@ -192,10 +162,26 @@ public void execute() throws IOException { "Bundled HMCLTransformerDiscoveryService is missing.")) { Files.createDirectories(file.getParent()); Files.copy(input, file, StandardCopyOption.REPLACE_EXISTING); + handled = true; + } + } else if (library.groupId().equals("modloader")) { + String url = switch (library.artifactId()) { + case "modloader" -> ForgeLegacyInstallTask.MODLOADER_DOWNLOAD_URL; + case "modloader-mp" -> ForgeLegacyInstallTask.MODLOADER_MP_DOWNLOAD_URL; + default -> null; + }; + if (url != null && shouldDownloadLibrary(gameRepository, manifest,library, integrityCheck)) { + var fileDownloadTask = new FileDownloadTask(url, file, null); + fileDownloadTask.setCacheRepository(dependencyManager.getCacheRepository()); + fileDownloadTask.setCaching(true); + fileDownloadTask.addIntegrityCheckHandler(FileDownloadTask.ZIP_INTEGRITY_CHECK_HANDLER); + dependencies.add(fileDownloadTask.withCounter("hmcl.install.libraries")); + + handled = true; } } - if (shouldDownloadLibrary(gameRepository, manifest, library, integrityCheck) && (library.hasDownloadURL() || !"optifine".equals(library.groupId()))) { + if (!handled && shouldDownloadLibrary(gameRepository, manifest, library, integrityCheck) && (library.hasDownloadURL() || !"optifine".equals(library.groupId()))) { dependencies.add(new LibraryDownloadTask(dependencyManager, file, library).withCounter("hmcl.install.libraries")); } else { dependencyManager.getCacheRepository().tryCacheLibrary(library, file); @@ -209,32 +195,4 @@ public void execute() throws IOException { notifyPropertiesChanged(); } } - - private static @Nullable List getFMLLibs(String forgeVersion) { - if (forgeVersion == null) - return null; - - // Minecraft 1.5.2 - if (forgeVersion.startsWith("7.8.1.")) { - return List.of( - new FMLLib("argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51"), - new FMLLib("guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", - "https://repo1.maven.org/maven2/com/google/guava/guava/14.0-rc3/guava-14.0-rc3.jar"), - new FMLLib("asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", - "https://repo1.maven.org/maven2/org/ow2/asm/asm-all/4.1/asm-all-4.1.jar"), - new FMLLib("bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", - "https://repo1.maven.org/maven2/org/bouncycastle/bcprov-jdk15on/1.48/bcprov-jdk15on-1.48.jar"), - new FMLLib("deobfuscation_data_1.5.2.zip", "446e55cd986582c70fcf12cb27bc00114c5adfd9"), - new FMLLib("scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85") - ); - } - - return null; - } - - private record FMLLib(String name, String sha1, String downloadUrl) { - FMLLib(String name, String sha1) { - this(name, sha1, "https://hmcl.glavo.site/metadata/fmllibs/" + name); - } - } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameVerificationFixTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameVerificationFixTask.java index 510073482fb..e83b19b3b42 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameVerificationFixTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameVerificationFixTask.java @@ -19,7 +19,6 @@ import org.jackhuang.hmcl.game.GameComponentType; import org.jackhuang.hmcl.game.GameInstance; -import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; @@ -41,18 +40,14 @@ public final class GameVerificationFixTask extends Task { /// The detected Minecraft version. private final GameVersionNumber gameVersion; - /// The effective launch manifest used to detect Forge. - private final GameInstanceManifest manifest; - /// Creates a task for a fixed instance and effective launch manifest. /// /// @param instance the instance whose client jar may be modified /// @param gameVersion the detected Minecraft version /// @param manifest the effective launch manifest used to detect Forge - public GameVerificationFixTask(GameInstance instance, GameVersionNumber gameVersion, GameInstanceManifest manifest) { + public GameVerificationFixTask(GameInstance instance, GameVersionNumber gameVersion) { this.instance = instance; this.gameVersion = gameVersion; - this.manifest = manifest; setSignificance(TaskSignificance.MODERATE); } @@ -68,6 +63,9 @@ public void execute() throws IOException { try (FileSystem fs = CompressingUtils.createWritableZipFileSystem(jar, StandardCharsets.UTF_8)) { Files.deleteIfExists(fs.getPath("META-INF/MOJANG_C.DSA")); Files.deleteIfExists(fs.getPath("META-INF/MOJANG_C.SF")); + Files.deleteIfExists(fs.getPath("META-INF/CODESIGN.SF")); + Files.deleteIfExists(fs.getPath("META-INF/CODESIGN.RSA")); + Files.deleteIfExists(fs.getPath("META-INF/MANIFEST.MF")); } } } 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 index 6181794c583..1e73bb5cdf8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeInstallTask.java @@ -19,7 +19,8 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.VersionMismatchException; -import org.jackhuang.hmcl.download.forge.*; +import org.jackhuang.hmcl.download.forge.ForgeNewInstallProfile; +import org.jackhuang.hmcl.download.forge.ForgeNewInstallTask; import org.jackhuang.hmcl.download.game.GameDownloadTask; import org.jackhuang.hmcl.game.GameComponentType; import org.jackhuang.hmcl.game.GameInstanceManifest; @@ -33,7 +34,9 @@ import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; -import java.util.*; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; import static org.jackhuang.hmcl.util.StringUtils.removePrefix; import static org.jackhuang.hmcl.util.StringUtils.removeSuffix; @@ -124,14 +127,14 @@ public static Task install( Map installProfile = JsonUtils.fromNonNullJson(installProfileText, Map.class); if (GameComponentType.FORGE.getPatchId().equals(installProfile.get("profile")) && (Files.exists(fs.getPath("META-INF/NEOFORGE.RSA")) || installProfileText.contains("neoforge"))) { ForgeNewInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeNewInstallProfile.class); - if (!gameVersion.equals(profile.getMinecraft())) - throw new VersionMismatchException(profile.getMinecraft(), gameVersion); + if (!gameVersion.equals(profile.minecraft())) + throw new VersionMismatchException(profile.minecraft(), gameVersion); return new GameDownloadTask(dependencyManager, manifest) .thenComposeAsync(minecraftJar -> new ForgeNewInstallTask( dependencyManager, manifest, minecraftJar, - modifyNeoForgeOldVersion(gameVersion, profile.getVersion()), + modifyNeoForgeOldVersion(gameVersion, profile.version()), installer)) .thenApplyAsync(neoForgeVersion -> { if (!neoForgeVersion.id().equals(GameComponentType.FORGE.getPatchId()) || neoForgeVersion.version() == null) { @@ -144,14 +147,14 @@ public static Task install( }); } else if (GameComponentType.NEO_FORGE.getPatchId().equals(installProfile.get("profile")) || "NeoForge".equals(installProfile.get("profile"))) { ForgeNewInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeNewInstallProfile.class); - if (!gameVersion.equals(profile.getMinecraft())) - throw new VersionMismatchException(profile.getMinecraft(), gameVersion); + if (!gameVersion.equals(profile.minecraft())) + throw new VersionMismatchException(profile.minecraft(), gameVersion); return new GameDownloadTask(dependencyManager, manifest) .thenComposeAsync(minecraftJar -> new NeoForgeOldInstallTask( dependencyManager, manifest, minecraftJar, - modifyNeoForgeNewVersion(profile.getVersion()), + modifyNeoForgeNewVersion(profile.version()), installer)); } else { throw new IOException(); 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 index 522729d4df5..dc9ae474b13 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeOldInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeOldInstallTask.java @@ -21,9 +21,10 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; 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.GameInstanceJsonDownloadTask; +import org.jackhuang.hmcl.download.game.GameLibrariesTask; import org.jackhuang.hmcl.game.*; +import org.jackhuang.hmcl.java.JavaRuntime; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.DigestUtils; @@ -34,7 +35,6 @@ 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.java.JavaRuntime; import org.jackhuang.hmcl.util.platform.SystemUtils; import org.jetbrains.annotations.NotNull; @@ -53,8 +53,8 @@ import java.util.jar.JarFile; import java.util.zip.ZipException; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.gson.JsonUtils.fromNonNullJson; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; public class NeoForgeOldInstallTask extends Task { @@ -74,7 +74,7 @@ public void execute() throws Exception { Map outputs = new HashMap<>(); boolean miss = false; - for (Map.Entry entry : processor.getOutputs().entrySet()) { + for (Map.Entry entry : processor.outputs().entrySet()) { String key = entry.getKey(); String value = entry.getValue(); @@ -105,11 +105,11 @@ public void execute() throws Exception { } } - if (!processor.getOutputs().isEmpty() && !miss) { + if (!processor.outputs().isEmpty() && !miss) { return; } - Path jar = gameRepository.getLayout().getArtifactFile(processor.getJar()); + Path jar = gameRepository.getLayout().getArtifactFile(processor.jar()); if (!Files.isRegularFile(jar)) throw new FileNotFoundException("Game processor file not found, should be downloaded in preprocess"); @@ -125,8 +125,8 @@ public void execute() throws Exception { command.add(JavaRuntime.getDefault().getBinary().toString()); command.add("-cp"); - List classpath = new ArrayList<>(processor.getClasspath().size() + 1); - for (Artifact artifact : processor.getClasspath()) { + List classpath = new ArrayList<>(processor.classpath().size() + 1); + for (Artifact artifact : processor.classpath()) { Path file = gameRepository.getLayout().getArtifactFile(artifact); if (!Files.isRegularFile(file)) throw new Exception("Game processor dependency missing"); @@ -137,8 +137,8 @@ public void execute() throws Exception { command.add(mainClass); - List args = new ArrayList<>(processor.getArgs().size()); - for (String arg : processor.getArgs()) { + List args = new ArrayList<>(processor.args().size()); + for (String arg : processor.args()) { String parsed = parseLiteral(arg, vars); if (parsed == null) throw new ArtifactMalformedException("Invalid forge installation configuration"); @@ -147,7 +147,7 @@ public void execute() throws Exception { command.addAll(args); - LOG.info("Executing external processor " + processor.getJar().toString() + ", command line: " + new CommandBuilder().addAll(command).toString()); + LOG.info("Executing external processor " + processor.jar().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); @@ -288,10 +288,10 @@ public boolean doPreExecute() { public void preExecute() throws Exception { try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) { profile = JsonUtils.fromNonNullJson(Files.readString(fs.getPath("install_profile.json")), ForgeNewInstallProfile.class); - processors = profile.getProcessors(); - neoForgeVersion = JsonUtils.fromNonNullJson(Files.readString(fs.getPath(profile.getJson())), GameInstanceManifest.class); + processors = profile.processors(); + neoForgeVersion = JsonUtils.fromNonNullJson(Files.readString(fs.getPath(profile.json())), GameInstanceManifest.class); - for (Library library : profile.getLibraries()) { + for (Library library : profile.libraries()) { Path file = fs.getPath("maven").resolve(library.getPath()); if (Files.exists(file)) { Path dest = gameRepository.getLayout().getLibraryFile(manifest.id(), library); @@ -310,7 +310,7 @@ public void preExecute() throws Exception { throw new ArtifactMalformedException("Malformed forge installer file", ex); } - dependents.add(new GameLibrariesTask(dependencyManager, manifest, true, profile.getLibraries())); + dependents.add(new GameLibrariesTask(dependencyManager, manifest, true, profile.libraries())); } private Map parseOptions(List args, Map vars) { @@ -338,7 +338,7 @@ private Map parseOptions(List args, Map } private Task patchDownloadMojangMappingsTask(Processor processor, Map vars) { - Map options = parseOptions(processor.getArgs(), vars); + Map options = parseOptions(processor.args(), vars); if (!"DOWNLOAD_MOJMAPS".equals(options.get("task")) || !"client".equals(options.get("side"))) return null; String version = options.get("version"); @@ -408,7 +408,7 @@ public void execute() throws Exception { vars.put("SIDE", "client"); vars.put("MINECRAFT_JAR", FileUtils.getAbsolutePath(isolatedMinecraftJar)); - vars.put("MINECRAFT_VERSION", profile.getMinecraft()); + vars.put("MINECRAFT_VERSION", profile.minecraft()); vars.put("ROOT", FileUtils.getAbsolutePath(gameRepository.getBaseDirectory())); vars.put("INSTALLER", installer.toAbsolutePath().toString()); vars.put("LIBRARY_DIR", FileUtils.getAbsolutePath(gameRepository.getLayout().getLibrariesDirectory())); diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java index 71856a047d0..892f5b3ad7f 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java @@ -52,13 +52,7 @@ import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotSame; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; /// Tests snapshot-bound behavior of [DefaultGameInstance]. @NotNullByDefault @@ -383,7 +377,7 @@ public void testVerificationFixKeepsCapturedInstance(@TempDir Path tempDirectory tempDirectory.resolve("versions/instance/current.json")); writeSignedJar(current.getInstanceJarFile()); - new GameVerificationFixTask(captured, GameVersionNumber.asGameVersion("1.5.2"), manifest).execute(); + new GameVerificationFixTask(captured, GameVersionNumber.asGameVersion("1.5.2")).execute(); assertFalse(hasZipEntry(captured.getInstanceJarFile(), "META-INF/MOJANG_C.DSA")); assertFalse(hasZipEntry(captured.getInstanceJarFile(), "META-INF/MOJANG_C.SF")); diff --git a/minecraft/libraries/HMCLLegacyForgeHelper/build.gradle.kts b/minecraft/libraries/HMCLLegacyForgeHelper/build.gradle.kts new file mode 100644 index 00000000000..be3b1061d77 --- /dev/null +++ b/minecraft/libraries/HMCLLegacyForgeHelper/build.gradle.kts @@ -0,0 +1,39 @@ +import java.io.RandomAccessFile + +version = "1.0" + +tasks.compileJava { + sourceCompatibility = "1.8" + targetCompatibility = "1.8" +} + +tasks.jar { + manifest { + attributes( + "Created-By" to "Copyright(c) 2026 huangyuhui.", + "Implementation-Version" to project.version, + "Premain-Class" to "org.jackhuang.hmcl.HMCLLegacyForgeHelper", + "Can-Redefine-Classes" to true, + "Can-Retransform-Classes" to true + ) + } +} + +tasks.compileJava { + doLast { + val outputDir = destinationDirectory.get().asFile + outputDir.walkTopDown() + .filter { it.isFile && it.extension == "class" } + .forEach { file -> + RandomAccessFile(file, "rw").use { raf -> + if (raf.length() >= 8) { + val magic = raf.readInt() + if (magic == 0xCAFEBABE.toInt()) { + raf.seek(6) + raf.writeShort(50) + } + } + } + } + } +} \ No newline at end of file diff --git a/minecraft/libraries/HMCLLegacyForgeHelper/src/main/java/org/jackhuang/hmcl/HMCLLegacyForgeHelper.java b/minecraft/libraries/HMCLLegacyForgeHelper/src/main/java/org/jackhuang/hmcl/HMCLLegacyForgeHelper.java new file mode 100644 index 00000000000..4e6e054603f --- /dev/null +++ b/minecraft/libraries/HMCLLegacyForgeHelper/src/main/java/org/jackhuang/hmcl/HMCLLegacyForgeHelper.java @@ -0,0 +1,152 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 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; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.Instrumentation; +import java.nio.charset.Charset; +import java.security.ProtectionDomain; + +@SuppressWarnings("JavaPrintToLogpoint") +public final class HMCLLegacyForgeHelper { + private HMCLLegacyForgeHelper() { + throw new AssertionError(); + } + + private static final String TARGET_URL = "http://files.minecraftforge.net/fmllibs/%s"; + + private static String newRootUrl = "https://hmcl.glavo.site/metadata/fmllibs/%s"; + + public static void premain(String agentArgs, Instrumentation inst) { + if (agentArgs != null && !agentArgs.trim().isEmpty()) { + newRootUrl = agentArgs.trim(); + } + + inst.addTransformer(new CoreFMLLibrariesTransformer()); + } + + public static void agentmain(String agentArgs, Instrumentation inst) { + premain(agentArgs, inst); + } + + private final static class CoreFMLLibrariesTransformer implements ClassFileTransformer { + + @Override + public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { + if (className == null) { + return null; + } + + if ("cpw/mods/fml/relauncher/CoreFMLLibraries".equals(className) || "cpw.mods.fml.relauncher.CoreFMLLibraries".equals(className)) { + try { + System.out.println("[LegacyForgeHelper] Transforming " + className + " ..."); + byte[] modified = patchConstantPoolUtf8(classfileBuffer, TARGET_URL, newRootUrl); + System.out.println("[LegacyForgeHelper] Successfully patched RootURL in " + className); + return modified; + } catch (Throwable t) { + System.err.println("[LegacyForgeHelper] Failed to patch class"); + t.printStackTrace(); + } + } + return null; + } + } + + private static byte[] patchConstantPoolUtf8(byte[] classBytes, String targetStr, String replacementStr) { + if (classBytes == null || classBytes.length < 10) { + return classBytes; + } + + int pos = 0; + + int magic = ((classBytes[pos++] & 0xFF) << 24) | ((classBytes[pos++] & 0xFF) << 16) | ((classBytes[pos++] & 0xFF) << 8) | (classBytes[pos++] & 0xFF); + if (magic != 0xCAFEBABE) { + throw new IllegalArgumentException("Invalid class file (magic mismatch)"); + } + + // minor_version & major_version + pos += 4; + + int cpCount = ((classBytes[pos++] & 0xFF) << 8) | (classBytes[pos++] & 0xFF); + + for (int i = 1; i < cpCount; i++) { + int tag = classBytes[pos++] & 0xFF; + switch (tag) { + case 1: // CONSTANT_Utf8 + int utf8LengthPos = pos; + int len = ((classBytes[pos++] & 0xFF) << 8) | (classBytes[pos++] & 0xFF); + String str = new String(classBytes, pos, len, Charset.forName("UTF-8")); + pos += len; + + if (targetStr.equals(str)) { + byte[] replacementBytes = replacementStr.getBytes(Charset.forName("UTF-8")); + if (replacementBytes.length > 65535) { + throw new IllegalArgumentException("Replacement URL is too long (max 65535 bytes)"); + } + + int beforeLen = utf8LengthPos; + int afterOffset = utf8LengthPos + 2 + len; + int afterLen = classBytes.length - afterOffset; + + byte[] newClass = new byte[beforeLen + 2 + replacementBytes.length + afterLen]; + + System.arraycopy(classBytes, 0, newClass, 0, beforeLen); + newClass[beforeLen] = (byte) ((replacementBytes.length >>> 8) & 0xFF); + newClass[beforeLen + 1] = (byte) (replacementBytes.length & 0xFF); + System.arraycopy(replacementBytes, 0, newClass, beforeLen + 2, replacementBytes.length); + System.arraycopy(classBytes, afterOffset, newClass, beforeLen + 2 + replacementBytes.length, afterLen); + + return newClass; + } + break; + + case 3: // CONSTANT_Integer + case 4: // CONSTANT_Float + case 9: // CONSTANT_Fieldref + case 10: // CONSTANT_Methodref + case 11: // CONSTANT_InterfaceMethodref + case 12: // CONSTANT_NameAndType + case 18: // CONSTANT_InvokeDynamic + pos += 4; + break; + + case 5: // CONSTANT_Long + case 6: // CONSTANT_Double + pos += 8; + i++; + break; + + case 7: // CONSTANT_Class + case 8: // CONSTANT_String + case 16: // CONSTANT_MethodType + pos += 2; + break; + + case 15: // CONSTANT_MethodHandle + pos += 3; + break; + + default: + throw new IllegalArgumentException("Unsupported constant pool tag: " + tag + " at pos " + (pos - 1)); + } + } + + System.out.println("[LegacyForgeHelper] Warning: Target URL string not found in constant pool."); + return classBytes; + } +} diff --git a/minecraft/libraries/HMCLModLoaderHelper/build.gradle.kts b/minecraft/libraries/HMCLModLoaderHelper/build.gradle.kts new file mode 100644 index 00000000000..ed306fba768 --- /dev/null +++ b/minecraft/libraries/HMCLModLoaderHelper/build.gradle.kts @@ -0,0 +1,39 @@ +import java.io.RandomAccessFile + +version = "1.0" + +tasks.compileJava { + sourceCompatibility = "1.8" + targetCompatibility = "1.8" +} + +tasks.jar { + manifest { + attributes( + "Created-By" to "Copyright(c) 2026 huangyuhui.", + "Implementation-Version" to project.version, + "Premain-Class" to "org.jackhuang.hmcl.HMCLModLoaderHelper", + "Can-Redefine-Classes" to true, + "Can-Retransform-Classes" to true + ) + } +} + +tasks.compileJava { + doLast { + val outputDir = destinationDirectory.get().asFile + outputDir.walkTopDown() + .filter { it.isFile && it.extension == "class" } + .forEach { file -> + RandomAccessFile(file, "rw").use { raf -> + if (raf.length() >= 8) { + val magic = raf.readInt() + if (magic == 0xCAFEBABE.toInt()) { + raf.seek(6) + raf.writeShort(50) + } + } + } + } + } +} \ No newline at end of file diff --git a/minecraft/libraries/HMCLModLoaderHelper/src/main/java/org/jackhuang/hmcl/HMCLModLoaderHelper.java b/minecraft/libraries/HMCLModLoaderHelper/src/main/java/org/jackhuang/hmcl/HMCLModLoaderHelper.java new file mode 100644 index 00000000000..e43c73d5bbc --- /dev/null +++ b/minecraft/libraries/HMCLModLoaderHelper/src/main/java/org/jackhuang/hmcl/HMCLModLoaderHelper.java @@ -0,0 +1,142 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 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; + +import java.io.File; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.Instrumentation; +import java.lang.reflect.Field; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.security.CodeSource; +import java.security.ProtectionDomain; + +public final class HMCLModLoaderHelper { + private HMCLModLoaderHelper() { + throw new AssertionError(); + } + + public static void premain(String agentArgs, Instrumentation inst) { + initAgent(inst); + } + + public static void agentmain(String agentArgs, Instrumentation inst) { + initAgent(inst); + } + + private static void initAgent(Instrumentation inst) { + for (Class loadedClass : inst.getAllLoadedClasses()) { + try { + fixProtectionDomain(loadedClass.getProtectionDomain()); + } catch (Throwable ignored) { + } + } + + inst.addTransformer(new ClassFileTransformer() { + @Override + public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { + if (protectionDomain != null) { + fixProtectionDomain(protectionDomain); + } + return null; + } + }, true); + } + + private static void fixProtectionDomain(ProtectionDomain pd) { + if (pd == null) return; + try { + CodeSource cs = pd.getCodeSource(); + if (cs != null) { + fixCodeSource(cs); + } + } catch (Throwable ignored) { + } + } + + private static void fixCodeSource(CodeSource cs) { + try { + URL location = cs.getLocation(); + if (location == null) return; + + URL fixedLocation = cleanUrl(location); + if (fixedLocation != null && !fixedLocation.equals(location)) { + Field locationField = CodeSource.class.getDeclaredField("location"); + locationField.setAccessible(true); + locationField.set(cs, fixedLocation); + } + } catch (Throwable t) { + t.printStackTrace(); + } + } + + private static URL cleanUrl(URL url) { + if (url == null) return null; + String urlString = url.toString(); + + if (urlString.startsWith("jar:") || "jar".equalsIgnoreCase(url.getProtocol())) { + String inner = urlString.substring(4); + int bangIndex = inner.indexOf('!'); + if (bangIndex != -1) { + inner = inner.substring(0, bangIndex); + } + try { + return new URL(inner); + } catch (MalformedURLException e) { + if (inner.startsWith("file:")) { + inner = inner.substring(5); + } + try { + return new File(inner).toURI().toURL(); + } catch (MalformedURLException ignored) { + } + } + } + + try { + URI uri = url.toURI(); + if (uri.isOpaque() || (uri.getScheme() != null && !uri.getScheme().equalsIgnoreCase("file"))) { + String path = url.getPath(); + if (path != null) { + if (path.startsWith("file:")) { + path = path.substring(5); + } + int bangIndex = path.indexOf('!'); + if (bangIndex != -1) { + path = path.substring(0, bangIndex); + } + return new File(path).toURI().toURL(); + } + } + } catch (Exception e) { + try { + String path = url.getPath(); + if (path != null) { + int bangIndex = path.indexOf('!'); + if (bangIndex != -1) { + path = path.substring(0, bangIndex); + } + return new File(path).toURI().toURL(); + } + } catch (Exception ignored) { + } + } + return url; + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index a1a710b39c0..bf7564a31ee 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,7 +5,8 @@ include( "HMCLBoot" ) -val minecraftLibraries = listOf("HMCLTransformerDiscoveryService", "HMCLMultiMCBootstrap") +val minecraftLibraries = + listOf("HMCLTransformerDiscoveryService", "HMCLMultiMCBootstrap", "HMCLLegacyForgeHelper", "HMCLModLoaderHelper") include(minecraftLibraries) for (library in minecraftLibraries) {