From 33d5aaef78170ed5fbcdb1e0ae284d5ae191a9b3 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Thu, 27 Aug 2026 18:34:06 +0800 Subject: [PATCH] update --- .../download/forge/ForgeBMCLVersionList.java | 53 +++++++++---- .../hmcl/download/forge/ForgeInstallTask.java | 76 ++++++++++++------- .../download/forge/ForgeRemoteVersion.java | 16 +++- .../forge/ForgeUniversalInstallTask.java | 74 ++++++++++++++++++ .../hmcl/download/forge/ForgeVersionList.java | 9 ++- .../hmcl/game/GameInstancePatch.java | 31 ++++++++ 6 files changed, 211 insertions(+), 48 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeUniversalInstallTask.java 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 14dbcaae3d8..8d7d9e64a6a 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 @@ -35,6 +35,7 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.function.Consumer; import static org.jackhuang.hmcl.util.Lang.mapOf; import static org.jackhuang.hmcl.util.Pair.pair; @@ -89,26 +90,35 @@ public Task refreshAsync(String gameVersion) { for (ForgeVersion version : forgeVersions) { 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()); - - 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(); - 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( + List installerUrls = new ArrayList<>(); + List universalUrls = new ArrayList<>(); + for (ForgeVersion.File file : version.getFiles()) { + String branch = toLookupBranch(gameVersion, version.getBranch()); + + 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(); + + Consumer> addUrls = urlList -> { + urlList.add("https://files.minecraftforge.net/maven/net/minecraftforge/forge/" + classifier + "/" + fileName1); + urlList.add("https://files.minecraftforge.net/maven/net/minecraftforge/forge/" + classifier + "-" + lookupVersion + "/" + fileName2); + urlList.add(NetworkUtils.withQuery("https://bmclapi2.bangbang93.com/forge/download", mapOf( pair("mcversion", version.getGameVersion()), pair("version", version.getVersion()), pair("branch", branch), pair("category", file.getCategory()), pair("format", file.getFormat()) ))); + }; + + if ("installer".equals(file.getCategory()) && "jar".equals(file.getFormat())) { + addUrls.accept(installerUrls); + } else if ("universal".equals(file.getCategory())) { + addUrls.accept(universalUrls); } + } - if (urls.isEmpty()) + if (installerUrls.isEmpty() && universalUrls.isEmpty()) continue; Instant releaseDate = null; @@ -120,8 +130,23 @@ public Task refreshAsync(String gameVersion) { } } - versions.put(gameVersion, new ForgeRemoteVersion( - fromLookupVersion(version.getGameVersion()), version.getVersion(), releaseDate, urls)); + if (!installerUrls.isEmpty()) { + versions.put(gameVersion, new ForgeRemoteVersion( + fromLookupVersion(version.getGameVersion()), + version.getVersion(), + releaseDate, + installerUrls, + ForgeRemoteVersion.FileType.INSTALLER + )); + } else { + versions.put(gameVersion, new ForgeRemoteVersion( + fromLookupVersion(version.getGameVersion()), + version.getVersion(), + releaseDate, + universalUrls, + ForgeRemoteVersion.FileType.UNIVERSAL + )); + } } } finally { lock.writeLock().unlock(); 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 8b6e089659f..68600e310fd 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 @@ -30,12 +30,14 @@ import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import java.io.IOException; +import java.io.StringReader; 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 java.util.Properties; import static org.jackhuang.hmcl.download.UnsupportedInstallationException.CLEANROOM_NOT_COMPATIBLE_WITH_FORGE; import static org.jackhuang.hmcl.download.UnsupportedInstallationException.UNSUPPORTED_LAUNCH_WRAPPER; @@ -50,7 +52,7 @@ public final class ForgeInstallTask extends Task { private final DefaultDependencyManager dependencyManager; private final GameInstanceManifest manifest; - private Path installer; + private Path jar; private final ForgeRemoteVersion remote; private FileDownloadTask dependent; private Task dependency; @@ -69,11 +71,13 @@ public boolean doPreExecute() { @Override public void preExecute() throws Exception { - installer = Files.createTempFile("forge-installer", ".jar"); + jar = remote.getFileType() == ForgeRemoteVersion.FileType.INSTALLER + ? Files.createTempFile("forge-installer", ".jar") + : Files.createTempFile("forge-universal", ".zip"); dependent = new FileDownloadTask( dependencyManager.getDownloadProvider().injectURLsWithCandidates(remote.getUrls()), - installer, null); + jar, null); dependent.setCacheRepository(dependencyManager.getCacheRepository()); dependent.setCaching(true); dependent.addIntegrityCheckHandler(FileDownloadTask.ZIP_INTEGRITY_CHECK_HANDLER); @@ -86,7 +90,7 @@ public boolean doPostExecute() { @Override public void postExecute() throws Exception { - Files.deleteIfExists(installer); + Files.deleteIfExists(jar); setResult(dependency.getResult()); } @@ -109,16 +113,18 @@ public void execute() throws IOException, VersionMismatchException, UnsupportedI throw new UnsupportedInstallationException(UNSUPPORTED_LAUNCH_WRAPPER); } - if (detectForgeInstallerType(remote.getGameVersion(), installer)) { + if (remote.getFileType() == ForgeRemoteVersion.FileType.UNIVERSAL) { + dependency = new ForgeUniversalInstallTask(dependencyManager, manifest, remote.getSelfVersion(), jar); + } else if (detectForgeInstallerType(remote.getGameVersion(), jar)) { dependency = new GameDownloadTask(dependencyManager, manifest) .thenComposeAsync(minecraftJar -> new ForgeNewInstallTask( dependencyManager, manifest, minecraftJar, remote.getSelfVersion(), - installer)); + jar)); } else { - dependency = new ForgeOldInstallTask(dependencyManager, manifest, remote.getSelfVersion(), installer); + dependency = new ForgeOldInstallTask(dependencyManager, manifest, remote.getSelfVersion(), jar); } } @@ -170,30 +176,42 @@ public static Task install( 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(dependencyManager, 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(dependencyManager, 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(); + Path installProfilePath = fs.getPath("install_profile.json"); + Path versionPropertiesPath = fs.getPath("forgeversion.properties"); + if (Files.isRegularFile(installProfilePath)) { + String installProfileText = Files.readString(installProfilePath); + Map installProfile = JsonUtils.fromNonNullJson(installProfileText, Map.class); + if (installProfile.containsKey("spec")) { + checkCleanroomCompatibility(dependencyManager, 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(dependencyManager, 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 if (Files.isRegularFile(versionPropertiesPath)) { + Properties prop = new Properties(); + prop.load(new StringReader(Files.readString(versionPropertiesPath))); + String major = prop.getProperty("forge.major.number"); + String minor = prop.getProperty("forge.minor.number"); + String revision = prop.getProperty("forge.revision.number"); + String build = prop.getProperty("forge.build.number"); + if (major != null && minor != null && revision != null && build != null) + return new ForgeUniversalInstallTask(dependencyManager, manifest, "%s.%s.%s.%s".formatted(major, minor, revision, build), installer); } } + throw new IOException(); } /// Rejects Forge installation when the manifest already contains Cleanroom. diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeRemoteVersion.java index f93607092c0..58c9a6a1202 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeRemoteVersion.java @@ -28,19 +28,33 @@ import java.util.List; public class ForgeRemoteVersion extends RemoteVersion { + + private final FileType fileType; + /** * Constructor. * * @param gameVersion the Minecraft version that this remote version suits. * @param selfVersion the version string of the remote version. * @param url the installer or universal jar original URL. + * @param fileType the type of the file. */ - public ForgeRemoteVersion(String gameVersion, String selfVersion, Instant releaseDate, List url) { + public ForgeRemoteVersion(String gameVersion, String selfVersion, Instant releaseDate, List url, FileType fileType) { super(GameComponentType.FORGE, gameVersion, selfVersion, releaseDate, url); + this.fileType = fileType; } @Override public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion) { return new ForgeInstallTask(dependencyManager, baseVersion, this); } + + public FileType getFileType() { + return fileType; + } + + public enum FileType { + INSTALLER, + UNIVERSAL + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeUniversalInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeUniversalInstallTask.java new file mode 100644 index 00000000000..073c801af19 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeUniversalInstallTask.java @@ -0,0 +1,74 @@ +/* + * 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.game.*; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.util.DigestUtils; +import org.jackhuang.hmcl.util.io.FileUtils; + +import java.nio.file.Path; +import java.util.List; + +public class ForgeUniversalInstallTask extends Task { + + private final DefaultDependencyManager dependencyManager; + private final GameInstanceManifest manifest; + private final Path universal; + private final String selfVersion; + + ForgeUniversalInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, String selfVersion, Path universal) { + this.dependencyManager = dependencyManager; + this.manifest = manifest; + this.universal = universal; + this.selfVersion = selfVersion; + + setSignificance(TaskSignificance.MAJOR); + } + + @Override + public void execute() throws Exception { + var lib = new Library( + new Artifact( + "net.minecraftforge", + "minecraftforge", + selfVersion, + null, + FileUtils.getExtension(universal) + ), + null, + null, + List.of(DigestUtils.digestToString("SHA-1", universal)), + null, + null, + null, + null, + null + ); + Path target = dependencyManager.getGameRepository().getLayout().getLibraryFile(manifest.id(), lib); + FileUtils.copyFile(universal, target); + + setResult(GameInstancePatch.fromLibraries( + List.of(lib), + GameComponentType.FORGE.getPatchId(), + selfVersion, + GameInstancePatch.PRIORITY_LOADER + )); + } +} 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 9d0941b2b2b..a7b6db60691 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 @@ -70,23 +70,24 @@ public Task refreshAsync() { ForgeVersion version = root.getNumber().get(v); if (version == null) continue; - String jar = null; + String installerJar = 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; + installerJar = root.getWebPath() + classifier + "/" + fileName; } - if (jar == null) + if (installerJar == null) continue; versions.put(gameVersion, new ForgeRemoteVersion( toLookupVersion(version.getGameVersion()), version.getVersion(), version.getModified() > 0 ? Instant.ofEpochSecond(version.getModified()) : null, - Collections.singletonList(jar) + Collections.singletonList(installerJar), + ForgeRemoteVersion.FileType.INSTALLER )); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstancePatch.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstancePatch.java index 06fd1c6985b..506f9ec6aa6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstancePatch.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstancePatch.java @@ -158,6 +158,37 @@ public static GameInstancePatch fromManifest( null); } + /// Creates a patch from manifest metadata. + public static GameInstancePatch fromLibraries( + List libraries, + String id, + @Nullable String version, + int priority) { + return new GameInstancePatch( + id, + version, + priority, + null, + null, + null, + null, + null, + null, + null, + null, + null, + libraries, + null, + null, + null, + null, + null, + null, + null, + null, + null); + } + /// Returns the patch priority. public int getPriority() { return priority == null ? Integer.MIN_VALUE : priority;