From 07a2885046b6b246275877fc92ffae3fdf528fc9 Mon Sep 17 00:00:00 2001 From: link-fgfgui Date: Sat, 23 May 2026 00:08:36 +0800 Subject: [PATCH 01/14] =?UTF-8?q?=E8=B7=B3=E8=BF=87overrides=E5=B7=B2?= =?UTF-8?q?=E6=9C=89=E6=96=87=E4=BB=B6=E4=B8=8B=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java index 682690c3656..5c3d40936b1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java @@ -23,6 +23,7 @@ import org.jackhuang.hmcl.mod.ModpackCompletionException; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.util.DigestUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.FileUtils; @@ -115,7 +116,8 @@ public void execute() throws Exception { if (!filePath.startsWith(runDirectory)) throw new IOException("Unsecure path: " + file.getPath()); - if (Files.exists(filePath)) + String sha1 = file.getHashes() != null ? file.getHashes().get("sha1") : null; + if (sha1 != null && Files.exists(filePath) && DigestUtils.digestToString("SHA-1", filePath).equalsIgnoreCase(sha1)) continue; if (modsDirectory.equals(filePath.getParent()) && this.modManager.hasSimpleMod(FileUtils.getName(filePath))) continue; From 399c2846f1f2153be1acd4568f48393b1060c030 Mon Sep 17 00:00:00 2001 From: link-fgfgui Date: Sat, 23 May 2026 00:37:49 +0800 Subject: [PATCH 02/14] =?UTF-8?q?mrpack=E6=95=B4=E5=90=88=E5=8C=85?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=A1=AB=E5=85=A5fileApi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modrinth/ModrinthModpackExportTask.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index b4f7d9fc29b..aef93e2ab1b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -31,6 +31,7 @@ import org.jackhuang.hmcl.mod.ModpackExportInfo; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.DigestUtils; +import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.Zipper; import org.jackhuang.hmcl.mod.LocalModFile; @@ -136,6 +137,7 @@ public void execute() throws Exception { Set filesInManifest = new HashSet<>(); String[] resourceDirs = {"resourcepacks", "shaderpacks", "mods"}; + String fileApi = info.getFileApi() == null ? null : StringUtils.removeSuffix(info.getFileApi(), "/"); for (String dir : resourceDirs) { Path dirPath = runDirectory.resolve(dir); if (Files.exists(dirPath)) { @@ -150,6 +152,23 @@ public void execute() throws Exception { } ModrinthManifest.File fileEntry = tryGetRemoteFile(file, relativePath); + if (fileEntry == null && fileApi != null) { + Map hashes = new HashMap<>(); + hashes.put("sha1", DigestUtils.digestToString("SHA-1", file)); + hashes.put("sha512", DigestUtils.digestToString("SHA-512", file)); + + long fileSize = Files.size(file); + if (fileSize > Integer.MAX_VALUE) { + LOG.warning("File " + relativePath + " is too large (size: " + fileSize + " bytes), precision may be lost when converting to int"); + } + fileEntry = new ModrinthManifest.File( + relativePath, + hashes, + null, + Collections.singletonList(fileApi + "/" + relativePath), + (int) fileSize + ); + } if (fileEntry != null) { files.add(fileEntry); filesInManifest.add(relativePath); From aad822fb8d7e871feafcef3bfa5d4b14af6474e4 Mon Sep 17 00:00:00 2001 From: link-fgfgui Date: Sat, 23 May 2026 01:02:04 +0800 Subject: [PATCH 03/14] =?UTF-8?q?=E5=A1=AB=E5=85=A5=E7=9A=84fileApi?= =?UTF-8?q?=E5=AD=98=E5=8F=96=E5=88=B0=E8=87=AA=E5=AE=9A=E4=B9=89fileApi?= =?UTF-8?q?=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/mod/modrinth/ModrinthManifest.java | 13 ++++++++++++- .../mod/modrinth/ModrinthModpackExportTask.java | 4 +++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthManifest.java index ee5ac48e6fa..4085ea346c9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthManifest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthManifest.java @@ -37,8 +37,9 @@ public class ModrinthManifest implements ModpackManifest, Validation { private final @Nullable String summary; private final List files; private final Map dependencies; + private final @Nullable String fileApi; - public ModrinthManifest(String game, int formatVersion, String versionId, String name, @Nullable String summary, List files, Map dependencies) { + public ModrinthManifest(String game, int formatVersion, String versionId, String name, @Nullable String summary, List files, Map dependencies, @Nullable String fileApi) { this.game = game; this.formatVersion = formatVersion; this.versionId = versionId; @@ -46,6 +47,11 @@ public ModrinthManifest(String game, int formatVersion, String versionId, String this.summary = summary; this.files = files; this.dependencies = dependencies; + this.fileApi = fileApi; + } + + public ModrinthManifest(String game, int formatVersion, String versionId, String name, @Nullable String summary, List files, Map dependencies) { + this(game, formatVersion, versionId, name, summary, files, dependencies, null); } public String getGame() { @@ -80,6 +86,11 @@ public String getGameVersion() { return dependencies.get("minecraft"); } + @Nullable + public String getFileApi() { + return fileApi; + } + @Override public ModpackProvider getProvider() { return ModrinthModpackProvider.INSTANCE; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index aef93e2ab1b..2c8ea63e827 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -211,7 +211,8 @@ public void execute() throws Exception { info.getName(), info.getDescription(), files, - dependencies + dependencies, + fileApi ); zip.putTextFile(JsonUtils.GSON.toJson(manifest), "modrinth.index.json"); @@ -219,6 +220,7 @@ public void execute() throws Exception { } public static final ModpackExportInfo.Options OPTION = new ModpackExportInfo.Options() + .requireFileApi(true) .requireNoCreateRemoteFiles() .requireSkipCurseForgeRemoteFiles(); } From 8f80390a61f8e00edb7ac617e3fc2632a6866fff Mon Sep 17 00:00:00 2001 From: link-fgfgui Date: Sat, 23 May 2026 17:42:38 +0800 Subject: [PATCH 04/14] =?UTF-8?q?=E4=BF=AE=E5=A4=8DfileApi=E5=88=A4?= =?UTF-8?q?=E7=A9=BA=E6=9D=A1=E4=BB=B6=EF=BC=8C=E5=AF=B9URL=E8=BF=9B?= =?UTF-8?q?=E8=A1=8C=E7=BC=96=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/mod/modrinth/ModrinthModpackExportTask.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index 2c8ea63e827..388b3376987 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -33,6 +33,7 @@ import org.jackhuang.hmcl.util.DigestUtils; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.io.Zipper; import org.jackhuang.hmcl.mod.LocalModFile; import org.jackhuang.hmcl.mod.RemoteMod; @@ -137,7 +138,7 @@ public void execute() throws Exception { Set filesInManifest = new HashSet<>(); String[] resourceDirs = {"resourcepacks", "shaderpacks", "mods"}; - String fileApi = info.getFileApi() == null ? null : StringUtils.removeSuffix(info.getFileApi(), "/"); + String fileApi = StringUtils.isBlank(info.getFileApi()) ? null : StringUtils.removeSuffix(info.getFileApi(), "/"); for (String dir : resourceDirs) { Path dirPath = runDirectory.resolve(dir); if (Files.exists(dirPath)) { @@ -165,7 +166,7 @@ public void execute() throws Exception { relativePath, hashes, null, - Collections.singletonList(fileApi + "/" + relativePath), + Collections.singletonList(fileApi + "/" + NetworkUtils.encodeLocation(relativePath)), (int) fileSize ); } From 9dfbd89f1c0e5f39a75914cf40cab983068c1378 Mon Sep 17 00:00:00 2001 From: link-fgfgui Date: Sat, 23 May 2026 17:50:03 +0800 Subject: [PATCH 05/14] =?UTF-8?q?=E5=90=AF=E5=8A=A8=E6=97=B6=E6=A3=80?= =?UTF-8?q?=E6=B5=8B=E7=89=88=E6=9C=AC=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/game/LauncherHelper.java | 12 ++++ .../resources/assets/lang/I18N.properties | 1 + .../resources/assets/lang/I18N_zh.properties | 1 + .../assets/lang/I18N_zh_CN.properties | 1 + .../mod/ModpackUpdateRequiredException.java | 38 +++++++++++++ .../mod/modrinth/ModrinthCompletionTask.java | 55 ++++++++++++++----- 6 files changed, 93 insertions(+), 15 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackUpdateRequiredException.java 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 4800d1ad645..95d5ed9bda1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -34,6 +34,7 @@ import org.jackhuang.hmcl.mod.ModpackCompletionException; import org.jackhuang.hmcl.mod.ModpackConfiguration; import org.jackhuang.hmcl.mod.ModpackProvider; +import org.jackhuang.hmcl.mod.ModpackUpdateRequiredException; import org.jackhuang.hmcl.setting.*; import org.jackhuang.hmcl.task.*; import org.jackhuang.hmcl.ui.*; @@ -299,6 +300,17 @@ public void onStop(boolean success, TaskExecutor executor) { message = i18n("modpack.type.curse.not_found"); else message = i18n("modpack.type.curse.error"); + } else if (ex instanceof ModpackUpdateRequiredException updateEx) { + Controllers.confirm(i18n("modpack.update.found", updateEx.getVersionId()), i18n("modpack.update"), MessageType.QUESTION, () -> { + try { + ModpackConfiguration config = ModpackHelper.readModpackConfiguration(profile.getRepository().getModpackConfiguration(selectedVersion)); + Task updateTask = ModpackHelper.getUpdateTask(profile, updateEx.getModpackFile(), java.nio.charset.StandardCharsets.UTF_8, selectedVersion, config); + Controllers.taskDialog(updateTask, i18n("modpack.update"), TaskCancellationAction.NORMAL); + } catch (Exception e) { + Controllers.dialog(StringUtils.getStackTrace(e), i18n("message.error"), MessageType.ERROR); + } + }, null); + return; } else if (ex instanceof PermissionException) { message = i18n("launch.failed.executable_permission"); } else if (ex instanceof ProcessCreationException) { diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index a18a8203d6b..c07fd455ba8 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -972,6 +972,7 @@ modpack.type.server.export=Allows server owner to remotely update the game insta modpack.type.server.malformed=Invalid modpack manifest. Please contact the modpack maker to resolve this problem. modpack.unsupported=Unsupported modpack format modpack.update=Updating modpack +modpack.update.found=A new version of the modpack is available: %s. Update now? modpack.wizard=Modpack Export Guide modpack.wizard.step.1=Basic Settings modpack.wizard.step.1.title=Some basic informations for the modpack. diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 24f40bff62c..951293840ca 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -779,6 +779,7 @@ modpack.type.server.export=允許伺服器管理員遠端更新遊戲用戶端 modpack.type.server.malformed=伺服器模組包配置格式錯誤,請聯絡伺服器管理員解決此問題 modpack.unsupported=Hello Minecraft! Launcher 不支援該模組包格式 modpack.update=正在升級模組包 +modpack.update.found=發現模組包新版本:%s,是否更新? modpack.wizard=匯出模組包引導 modpack.wizard.step.1=基本設定 modpack.wizard.step.1.title=設定模組包的主要訊息 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 341793b1a0c..28117c8bb89 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -784,6 +784,7 @@ modpack.type.server.export=允许服务器管理员远程更新游戏客户端 modpack.type.server.malformed=服务器整合包配置格式错误,请联系服务器管理员解决此问题。\n如遇到问题,你可以点击右上角帮助按钮进行求助。 modpack.unsupported=Hello Minecraft! Launcher 不支持该整合包格式 modpack.update=正在升级整合包 +modpack.update.found=发现整合包新版本:%s,是否更新? modpack.wizard=导出整合包向导 modpack.wizard.step.1=基本设置 modpack.wizard.step.1.title=设置整合包的主要信息 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackUpdateRequiredException.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackUpdateRequiredException.java new file mode 100644 index 00000000000..b0a3b10ddeb --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackUpdateRequiredException.java @@ -0,0 +1,38 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 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.mod; + +import java.nio.file.Path; + +public class ModpackUpdateRequiredException extends Exception { + private final Path modpackFile; + private final String versionId; + + public ModpackUpdateRequiredException(Path modpackFile, String versionId) { + this.modpackFile = modpackFile; + this.versionId = versionId; + } + + public Path getModpackFile() { + return modpackFile; + } + + public String getVersionId() { + return versionId; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java index 5c3d40936b1..59f22f516ad 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java @@ -21,11 +21,16 @@ import org.jackhuang.hmcl.game.DefaultGameRepository; import org.jackhuang.hmcl.mod.ModManager; import org.jackhuang.hmcl.mod.ModpackCompletionException; +import org.jackhuang.hmcl.mod.ModpackUpdateRequiredException; +import org.jackhuang.hmcl.task.CacheFileTask; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.DigestUtils; +import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.util.versioning.VersionNumber; import java.io.FileNotFoundException; import java.io.IOException; @@ -52,23 +57,12 @@ public class ModrinthCompletionTask extends Task { private final AtomicInteger finished = new AtomicInteger(0); private final AtomicBoolean notFound = new AtomicBoolean(false); - /** - * Constructor. - * - * @param dependencyManager the dependency manager. - * @param version the existent and physical version. - */ + private CacheFileTask downloadServerMrpackTask; + public ModrinthCompletionTask(DefaultDependencyManager dependencyManager, String version) { this(dependencyManager, version, null); } - /** - * Constructor. - * - * @param dependencyManager the dependency manager. - * @param version the existent and physical version. - * @param manifest the CurseForgeModpack manifest. - */ public ModrinthCompletionTask(DefaultDependencyManager dependencyManager, String version, ModrinthManifest manifest) { this.dependency = dependencyManager; this.repository = dependencyManager.getGameRepository(); @@ -88,6 +82,24 @@ public ModrinthCompletionTask(DefaultDependencyManager dependencyManager, String setStage("hmcl.modpack.download"); } + @Override + public boolean doPreExecute() { + return true; + } + + @Override + public void preExecute() throws Exception { + if (manifest == null || StringUtils.isBlank(manifest.getFileApi())) return; + + downloadServerMrpackTask = new CacheFileTask( + dependency.getDownloadProvider().injectURLWithCandidates(manifest.getFileApi() + "/server.mrpack")); + } + + @Override + public Collection> getDependents() { + return downloadServerMrpackTask == null ? List.of() : List.of(downloadServerMrpackTask); + } + @Override public Collection> getDependencies() { return dependencies; @@ -103,6 +115,21 @@ public void execute() throws Exception { if (manifest == null) return; + if (downloadServerMrpackTask != null) { + Path serverMrpack = downloadServerMrpackTask.getResult(); + + ModrinthManifest remoteManifest; + try (var zip = CompressingUtils.openZipFile(serverMrpack)) { + remoteManifest = JsonUtils.fromNonNullJson( + CompressingUtils.readTextZipEntry(zip, "modrinth.index.json"), + ModrinthManifest.class); + } + + if (VersionNumber.compare(remoteManifest.getVersionId(), manifest.getVersionId()) > 0) { + throw new ModpackUpdateRequiredException(serverMrpack, remoteManifest.getVersionId()); + } + } + Path runDirectory = FileUtils.toAbsolute(repository.getRunDirectory(version)); Path modsDirectory = runDirectory.resolve("mods"); @@ -143,8 +170,6 @@ public boolean doPostExecute() { @Override public void postExecute() throws Exception { - // Let this task fail if the curse manifest has not been completed. - // But continue other downloads. if (notFound.get()) throw new ModpackCompletionException(new FileNotFoundException()); if (!allNameKnown.get() || !isDependenciesSucceeded()) From 47f6919eed407a9b0f443dd51ea9c808c0b2a6e2 Mon Sep 17 00:00:00 2001 From: link-fgfgui Date: Sun, 24 May 2026 00:47:40 +0800 Subject: [PATCH 06/14] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modrinth/ModrinthModpackExportTask.java | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index 388b3376987..20b2d733318 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -23,6 +23,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; +import java.util.stream.Stream; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.game.DefaultGameRepository; @@ -135,13 +136,14 @@ public void execute() throws Exception { try (var zip = new Zipper(modpackFile)) { Path runDirectory = repository.getRunDirectory(version); List files = new ArrayList<>(); - Set filesInManifest = new HashSet<>(); + Set filesInManifest = new HashSet<>(); // a set contains the value of key "path" in every element of files String[] resourceDirs = {"resourcepacks", "shaderpacks", "mods"}; String fileApi = StringUtils.isBlank(info.getFileApi()) ? null : StringUtils.removeSuffix(info.getFileApi(), "/"); - for (String dir : resourceDirs) { - Path dirPath = runDirectory.resolve(dir); + try (Stream stream = Files.list(runDirectory)) { + for (Path dirPath : (Iterable) stream::iterator) { if (Files.exists(dirPath)) { + boolean isValidDir = Arrays.asList(resourceDirs).contains(dirPath.getFileName().toString()); // allow remote file match Files.walk(dirPath) .filter(Files::isRegularFile) .forEach(file -> { @@ -152,33 +154,38 @@ public void execute() throws Exception { return; } - ModrinthManifest.File fileEntry = tryGetRemoteFile(file, relativePath); - if (fileEntry == null && fileApi != null) { - Map hashes = new HashMap<>(); - hashes.put("sha1", DigestUtils.digestToString("SHA-1", file)); - hashes.put("sha512", DigestUtils.digestToString("SHA-512", file)); - - long fileSize = Files.size(file); - if (fileSize > Integer.MAX_VALUE) { - LOG.warning("File " + relativePath + " is too large (size: " + fileSize + " bytes), precision may be lost when converting to int"); - } - fileEntry = new ModrinthManifest.File( - relativePath, - hashes, - null, - Collections.singletonList(fileApi + "/" + NetworkUtils.encodeLocation(relativePath)), - (int) fileSize - ); + ModrinthManifest.File fileEntry = null; + if (isValidDir){ + fileEntry = tryGetRemoteFile(file, relativePath); } if (fileEntry != null) { files.add(fileEntry); filesInManifest.add(relativePath); + } else { + if (fileApi != null) { + Map hashes = new HashMap<>(); + hashes.put("sha1", DigestUtils.digestToString("SHA-1", file)); + hashes.put("sha512", DigestUtils.digestToString("SHA-512", file)); + + long fileSize = Files.size(file); + if (fileSize > Integer.MAX_VALUE) { + LOG.warning("File " + relativePath + " is too large (size: " + fileSize + " bytes), precision may be lost when converting to int"); + } + files.add(new ModrinthManifest.File( + relativePath, + hashes, + null, + Collections.singletonList(fileApi + "/" + NetworkUtils.encodeLocation(relativePath)), + (int) fileSize + )); + } } } catch (IOException e) { LOG.warning("Failed to process file: " + file, e); } }); } + } } zip.putDirectory(runDirectory, "client-overrides", path -> { From df1c931c0e16facc406e734da72514df75d46e2c Mon Sep 17 00:00:00 2001 From: link-fgfgui Date: Sun, 24 May 2026 01:22:46 +0800 Subject: [PATCH 07/14] =?UTF-8?q?=E8=A1=A5=E5=9B=9E=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E7=9A=84=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/mod/ModpackUpdateRequiredException.java | 2 +- .../mod/modrinth/ModrinthCompletionTask.java | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackUpdateRequiredException.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackUpdateRequiredException.java index b0a3b10ddeb..77240824c73 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackUpdateRequiredException.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackUpdateRequiredException.java @@ -1,6 +1,6 @@ /* * Hello Minecraft! Launcher - * Copyright (C) 2025 huangyuhui and contributors + * 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 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java index 59f22f516ad..ef87b3d7d2f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java @@ -58,11 +58,23 @@ public class ModrinthCompletionTask extends Task { private final AtomicBoolean notFound = new AtomicBoolean(false); private CacheFileTask downloadServerMrpackTask; - + /** + * Constructor. + * + * @param dependencyManager the dependency manager. + * @param version the existent and physical version. + */ public ModrinthCompletionTask(DefaultDependencyManager dependencyManager, String version) { this(dependencyManager, version, null); } + /** + * Constructor. + * + * @param dependencyManager the dependency manager. + * @param version the existent and physical version. + * @param manifest the CurseForgeModpack manifest. + */ public ModrinthCompletionTask(DefaultDependencyManager dependencyManager, String version, ModrinthManifest manifest) { this.dependency = dependencyManager; this.repository = dependencyManager.getGameRepository(); @@ -170,6 +182,8 @@ public boolean doPostExecute() { @Override public void postExecute() throws Exception { + // Let this task fail if the curse manifest has not been completed. + // But continue other downloads. if (notFound.get()) throw new ModpackCompletionException(new FileNotFoundException()); if (!allNameKnown.get() || !isDependenciesSucceeded()) From 13eff2e1562227d2fe6bd7417e44bfa2cc075b43 Mon Sep 17 00:00:00 2001 From: link-fgfgui Date: Sun, 24 May 2026 02:53:35 +0800 Subject: [PATCH 08/14] =?UTF-8?q?=E4=BD=BF=E7=94=A8FileDownloadTask?= =?UTF-8?q?=E8=80=8C=E9=9D=9ECacheFileTask=E9=81=BF=E5=85=8D=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E4=B8=8D=E5=8F=8A=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mod/modrinth/ModrinthCompletionTask.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java index ef87b3d7d2f..a8a4b3ac1f4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java @@ -22,7 +22,6 @@ import org.jackhuang.hmcl.mod.ModManager; import org.jackhuang.hmcl.mod.ModpackCompletionException; import org.jackhuang.hmcl.mod.ModpackUpdateRequiredException; -import org.jackhuang.hmcl.task.CacheFileTask; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.DigestUtils; @@ -57,7 +56,8 @@ public class ModrinthCompletionTask extends Task { private final AtomicInteger finished = new AtomicInteger(0); private final AtomicBoolean notFound = new AtomicBoolean(false); - private CacheFileTask downloadServerMrpackTask; + private FileDownloadTask downloadServerMrpackTask; + private Path serverMrpackTempFile; /** * Constructor. * @@ -103,8 +103,10 @@ public boolean doPreExecute() { public void preExecute() throws Exception { if (manifest == null || StringUtils.isBlank(manifest.getFileApi())) return; - downloadServerMrpackTask = new CacheFileTask( - dependency.getDownloadProvider().injectURLWithCandidates(manifest.getFileApi() + "/server.mrpack")); + serverMrpackTempFile = Files.createTempFile("hmcl-server-auto-update-pack", ".mrpack"); + downloadServerMrpackTask = new FileDownloadTask( + manifest.getFileApi() + "/server.mrpack", + serverMrpackTempFile); } @Override @@ -128,18 +130,19 @@ public void execute() throws Exception { return; if (downloadServerMrpackTask != null) { - Path serverMrpack = downloadServerMrpackTask.getResult(); - ModrinthManifest remoteManifest; - try (var zip = CompressingUtils.openZipFile(serverMrpack)) { + try (var zip = CompressingUtils.openZipFile(serverMrpackTempFile)) { remoteManifest = JsonUtils.fromNonNullJson( CompressingUtils.readTextZipEntry(zip, "modrinth.index.json"), ModrinthManifest.class); } if (VersionNumber.compare(remoteManifest.getVersionId(), manifest.getVersionId()) > 0) { - throw new ModpackUpdateRequiredException(serverMrpack, remoteManifest.getVersionId()); + throw new ModpackUpdateRequiredException(serverMrpackTempFile, remoteManifest.getVersionId()); } + + Files.deleteIfExists(serverMrpackTempFile); + serverMrpackTempFile = null; } Path runDirectory = FileUtils.toAbsolute(repository.getRunDirectory(version)); From 72a3dfb1025fe23d1633a36233b2983e90336e0f Mon Sep 17 00:00:00 2001 From: link-fgfgui Date: Sun, 24 May 2026 04:43:51 +0800 Subject: [PATCH 09/14] =?UTF-8?q?=E6=9B=B4=E6=96=B0Modrinth=E6=95=B4?= =?UTF-8?q?=E5=90=88=E5=8C=85=E6=97=B6=EF=BC=8C=E8=8B=A5modloader=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=AE=8C=E5=85=A8=E4=B8=80=E8=87=B4=E5=88=99=E8=B7=B3?= =?UTF-8?q?=E8=BF=87modloader=E7=9A=84=E9=87=8D=E6=96=B0=E5=AE=89=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mod/modrinth/ModrinthInstallTask.java | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthInstallTask.java index c977b101766..7059c77f979 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthInstallTask.java @@ -90,16 +90,6 @@ public ModrinthInstallTask(DefaultDependencyManager dependencyManager, Path zipF throw new IllegalStateException("Unsupported mod loader " + modLoader.getKey()); } } - dependents.add(builder.buildAsync()); - - onDone().register(event -> { - Exception ex = event.getTask().getException(); - if (event.isFailed()) { - if (!(ex instanceof ModpackCompletionException)) { - repository.removeVersionFromDisk(name); - } - } - }); ModpackConfiguration config = null; try { @@ -111,8 +101,21 @@ public ModrinthInstallTask(DefaultDependencyManager dependencyManager, Path zipF } } catch (JsonParseException | IOException ignore) { } - this.config = config; + + if (config == null || !config.getManifest().getDependencies().equals(manifest.getDependencies())) { + dependents.add(builder.buildAsync()); + } + + onDone().register(event -> { + Exception ex = event.getTask().getException(); + if (event.isFailed()) { + if (!(ex instanceof ModpackCompletionException)) { + repository.removeVersionFromDisk(name); + } + } + }); + List subDirectories = Arrays.asList("/client-overrides", "/overrides"); dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), subDirectories, any -> true, config).withStage("hmcl.modpack")); dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), subDirectories, manifest, ModrinthModpackProvider.INSTANCE, manifest.getName(), manifest.getVersionId(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack")); From 68d00b2288d6ffc5140e28550f4512740f82a0b0 Mon Sep 17 00:00:00 2001 From: link-fgfgui Date: Sun, 24 May 2026 14:00:07 +0800 Subject: [PATCH 10/14] =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=A3=80=E6=B5=8B?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/game/LauncherHelper.java | 56 ++++++--- .../resources/assets/lang/I18N.properties | 2 +- .../resources/assets/lang/I18N_zh.properties | 2 +- .../assets/lang/I18N_zh_CN.properties | 2 +- .../mod/ModpackUpdateRequiredException.java | 38 ------ .../ModrinthCheckServerPackUpdateTask.java | 110 ++++++++++++++++++ .../mod/modrinth/ModrinthCompletionTask.java | 42 ------- .../modrinth/ModrinthModpackExportTask.java | 3 +- 8 files changed, 157 insertions(+), 98 deletions(-) delete mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackUpdateRequiredException.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCheckServerPackUpdateTask.java 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 95d5ed9bda1..51e6477d066 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -34,7 +34,9 @@ import org.jackhuang.hmcl.mod.ModpackCompletionException; import org.jackhuang.hmcl.mod.ModpackConfiguration; import org.jackhuang.hmcl.mod.ModpackProvider; -import org.jackhuang.hmcl.mod.ModpackUpdateRequiredException; +import org.jackhuang.hmcl.mod.modrinth.ModrinthCheckServerPackUpdateTask; +import org.jackhuang.hmcl.mod.modrinth.ModrinthManifest; +import org.jackhuang.hmcl.mod.modrinth.ModrinthModpackProvider; import org.jackhuang.hmcl.setting.*; import org.jackhuang.hmcl.task.*; import org.jackhuang.hmcl.ui.*; @@ -44,6 +46,7 @@ import org.jackhuang.hmcl.ui.construct.PromptDialogPane; import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane; import org.jackhuang.hmcl.util.*; +import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.ResponseCodeException; @@ -57,6 +60,7 @@ import java.net.SocketTimeoutException; import java.net.URI; import java.nio.file.AccessDeniedException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.concurrent.*; @@ -158,6 +162,43 @@ private void launch0() { .thenComposeAsync(java -> { javaVersionRef.set(Objects.requireNonNull(java)); version.set(NativePatcher.patchNative(repository, version.get(), gameVersion.orElse(null), java, setting, javaArguments)); + return null; + }) + .thenComposeAsync(() -> { + try { + ModpackConfiguration configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion)); + if (ModrinthModpackProvider.INSTANCE.getName().equals(configuration.getType())) { + Path manifestFile = repository.getVersionRoot(selectedVersion).resolve("modrinth.index.json"); + if (Files.exists(manifestFile)) { + ModrinthManifest manifest = JsonUtils.fromJsonFile(manifestFile, ModrinthManifest.class); + if (StringUtils.isNotBlank(manifest.getFileApi())) { + return new ModrinthCheckServerPackUpdateTask(manifest.getVersionId(), manifest.getFileApi()); + } + } + } + } catch (IOException ignored) { + } + return null; + }) + .thenComposeAsync(hasUpdate -> { + if (hasUpdate == null || !hasUpdate) return null; + + CompletableFuture future = new CompletableFuture<>(); + runInFX(() -> { + Controllers.confirm(i18n("modpack.update.found"), i18n("modpack.update"), MessageType.QUESTION, () -> { + try { + ModpackConfiguration config = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion)); + Task updateTask = ModpackHelper.getUpdateTask(profile, ModrinthCheckServerPackUpdateTask.getUpdateFile(), java.nio.charset.StandardCharsets.UTF_8, selectedVersion, config); + Controllers.taskDialog(updateTask, i18n("modpack.update"), TaskCancellationAction.NORMAL); + } catch (Exception e) { + Controllers.dialog(StringUtils.getStackTrace(e), i18n("message.error"), MessageType.ERROR); + } + future.completeExceptionally(new CancellationException()); + }, () -> future.complete(null)); + }); + return Task.fromCompletableFuture(future); + }) + .thenComposeAsync(() -> { if (setting.isNotCheckGame()) return null; return Task.allOf( @@ -178,7 +219,7 @@ private void launch0() { || renderer.mesaDriverName() == null) return null; - Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.SYSTEM_VERSION); + Library lib = NativePatcher.getWindowsMesaLoader(javaVersionRef.get(), renderer, OperatingSystem.SYSTEM_VERSION); if (lib == null) return null; Path file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib); @@ -300,17 +341,6 @@ public void onStop(boolean success, TaskExecutor executor) { message = i18n("modpack.type.curse.not_found"); else message = i18n("modpack.type.curse.error"); - } else if (ex instanceof ModpackUpdateRequiredException updateEx) { - Controllers.confirm(i18n("modpack.update.found", updateEx.getVersionId()), i18n("modpack.update"), MessageType.QUESTION, () -> { - try { - ModpackConfiguration config = ModpackHelper.readModpackConfiguration(profile.getRepository().getModpackConfiguration(selectedVersion)); - Task updateTask = ModpackHelper.getUpdateTask(profile, updateEx.getModpackFile(), java.nio.charset.StandardCharsets.UTF_8, selectedVersion, config); - Controllers.taskDialog(updateTask, i18n("modpack.update"), TaskCancellationAction.NORMAL); - } catch (Exception e) { - Controllers.dialog(StringUtils.getStackTrace(e), i18n("message.error"), MessageType.ERROR); - } - }, null); - return; } else if (ex instanceof PermissionException) { message = i18n("launch.failed.executable_permission"); } else if (ex instanceof ProcessCreationException) { diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index c07fd455ba8..ff8427404d5 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -972,7 +972,7 @@ modpack.type.server.export=Allows server owner to remotely update the game insta modpack.type.server.malformed=Invalid modpack manifest. Please contact the modpack maker to resolve this problem. modpack.unsupported=Unsupported modpack format modpack.update=Updating modpack -modpack.update.found=A new version of the modpack is available: %s. Update now? +modpack.update.found=A new version of the modpack is available. Update now? modpack.wizard=Modpack Export Guide modpack.wizard.step.1=Basic Settings modpack.wizard.step.1.title=Some basic informations for the modpack. diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 951293840ca..7ee47ebba25 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -779,7 +779,7 @@ modpack.type.server.export=允許伺服器管理員遠端更新遊戲用戶端 modpack.type.server.malformed=伺服器模組包配置格式錯誤,請聯絡伺服器管理員解決此問題 modpack.unsupported=Hello Minecraft! Launcher 不支援該模組包格式 modpack.update=正在升級模組包 -modpack.update.found=發現模組包新版本:%s,是否更新? +modpack.update.found=發現模組包新版本,是否更新? modpack.wizard=匯出模組包引導 modpack.wizard.step.1=基本設定 modpack.wizard.step.1.title=設定模組包的主要訊息 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 28117c8bb89..7a5c2e1e2cf 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -784,7 +784,7 @@ modpack.type.server.export=允许服务器管理员远程更新游戏客户端 modpack.type.server.malformed=服务器整合包配置格式错误,请联系服务器管理员解决此问题。\n如遇到问题,你可以点击右上角帮助按钮进行求助。 modpack.unsupported=Hello Minecraft! Launcher 不支持该整合包格式 modpack.update=正在升级整合包 -modpack.update.found=发现整合包新版本:%s,是否更新? +modpack.update.found=发现整合包新版本,是否更新? modpack.wizard=导出整合包向导 modpack.wizard.step.1=基本设置 modpack.wizard.step.1.title=设置整合包的主要信息 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackUpdateRequiredException.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackUpdateRequiredException.java deleted file mode 100644 index 77240824c73..00000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackUpdateRequiredException.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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.mod; - -import java.nio.file.Path; - -public class ModpackUpdateRequiredException extends Exception { - private final Path modpackFile; - private final String versionId; - - public ModpackUpdateRequiredException(Path modpackFile, String versionId) { - this.modpackFile = modpackFile; - this.versionId = versionId; - } - - public Path getModpackFile() { - return modpackFile; - } - - public String getVersionId() { - return versionId; - } -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCheckServerPackUpdateTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCheckServerPackUpdateTask.java new file mode 100644 index 00000000000..59843349a08 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCheckServerPackUpdateTask.java @@ -0,0 +1,110 @@ +/* + * 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.mod.modrinth; + +import org.jackhuang.hmcl.task.FileDownloadTask; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.io.CompressingUtils; +import org.jackhuang.hmcl.util.versioning.VersionNumber; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Collections; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +/// Checks whether a Modrinth modpack with `fileApi` has a newer version available on the remote server. +/// +/// The result is `true` if an update is available, `false` otherwise. When an update is found, +/// the downloaded `server.mrpack` path and remote version id are stored in static fields and can +/// be retrieved via [getPendingUpdateFile] and [getPendingUpdateVersionId]. +/// Call [consumePendingUpdate] to retrieve and clear the pending update info. +public class ModrinthCheckServerPackUpdateTask extends Task { + + private static @Nullable Path updateFilePath = null; + + private final String versionId; + private final FileDownloadTask downloadTask; + + public ModrinthCheckServerPackUpdateTask(String versionId, String fileApi) { + this.versionId = versionId; + + if (StringUtils.isNotBlank(fileApi)) { + try { + updateFilePath = Files.createTempFile("hmcl-server-auto-update", ".mrpack"); + downloadTask = new FileDownloadTask(fileApi + "/server.mrpack", updateFilePath); + } catch (IOException e) { + throw new RuntimeException(e); + } + } else { + downloadTask = null; + } + } + + @Override + public Collection> getDependents() { + return downloadTask != null ? Collections.singleton(downloadTask) : Collections.emptySet(); + } + + @Override + public boolean isRelyingOnDependents() { + return false; + } + + public static @Nullable Path getUpdateFile() { + return updateFilePath; + } + + @Override + public void execute() throws Exception { + if (updateFilePath == null || downloadTask == null || downloadTask.getException() != null) { + setResult(false); + return; + } + + try { + ModrinthManifest remoteManifest; + try (var zip = CompressingUtils.openZipFile(updateFilePath)) { + remoteManifest = JsonUtils.fromNonNullJson( + CompressingUtils.readTextZipEntry(zip, "modrinth.index.json"), + ModrinthManifest.class); + } + + if (VersionNumber.compare(remoteManifest.getVersionId(), versionId) > 0) { + setResult(true); + } else { + setResult(false); + } + } catch (Exception e) { + LOG.warning("Failed to check for modpack updates", e); + setResult(false); + } finally { + if (!getResult()) { + try { + Files.deleteIfExists(updateFilePath); + } catch (IOException ignored) { + } + } + } + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java index a8a4b3ac1f4..5c3d40936b1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java @@ -21,15 +21,11 @@ import org.jackhuang.hmcl.game.DefaultGameRepository; import org.jackhuang.hmcl.mod.ModManager; import org.jackhuang.hmcl.mod.ModpackCompletionException; -import org.jackhuang.hmcl.mod.ModpackUpdateRequiredException; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.DigestUtils; -import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; -import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; -import org.jackhuang.hmcl.util.versioning.VersionNumber; import java.io.FileNotFoundException; import java.io.IOException; @@ -56,8 +52,6 @@ public class ModrinthCompletionTask extends Task { private final AtomicInteger finished = new AtomicInteger(0); private final AtomicBoolean notFound = new AtomicBoolean(false); - private FileDownloadTask downloadServerMrpackTask; - private Path serverMrpackTempFile; /** * Constructor. * @@ -94,26 +88,6 @@ public ModrinthCompletionTask(DefaultDependencyManager dependencyManager, String setStage("hmcl.modpack.download"); } - @Override - public boolean doPreExecute() { - return true; - } - - @Override - public void preExecute() throws Exception { - if (manifest == null || StringUtils.isBlank(manifest.getFileApi())) return; - - serverMrpackTempFile = Files.createTempFile("hmcl-server-auto-update-pack", ".mrpack"); - downloadServerMrpackTask = new FileDownloadTask( - manifest.getFileApi() + "/server.mrpack", - serverMrpackTempFile); - } - - @Override - public Collection> getDependents() { - return downloadServerMrpackTask == null ? List.of() : List.of(downloadServerMrpackTask); - } - @Override public Collection> getDependencies() { return dependencies; @@ -129,22 +103,6 @@ public void execute() throws Exception { if (manifest == null) return; - if (downloadServerMrpackTask != null) { - ModrinthManifest remoteManifest; - try (var zip = CompressingUtils.openZipFile(serverMrpackTempFile)) { - remoteManifest = JsonUtils.fromNonNullJson( - CompressingUtils.readTextZipEntry(zip, "modrinth.index.json"), - ModrinthManifest.class); - } - - if (VersionNumber.compare(remoteManifest.getVersionId(), manifest.getVersionId()) > 0) { - throw new ModpackUpdateRequiredException(serverMrpackTempFile, remoteManifest.getVersionId()); - } - - Files.deleteIfExists(serverMrpackTempFile); - serverMrpackTempFile = null; - } - Path runDirectory = FileUtils.toAbsolute(repository.getRunDirectory(version)); Path modsDirectory = runDirectory.resolve("mods"); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index 20b2d733318..4b8aa7b34f4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -219,8 +219,7 @@ public void execute() throws Exception { info.getName(), info.getDescription(), files, - dependencies, - fileApi + dependencies ); zip.putTextFile(JsonUtils.GSON.toJson(manifest), "modrinth.index.json"); From a73aa549e3ea6d3481b357106910ad03efa82d0f Mon Sep 17 00:00:00 2001 From: link-fgfgui Date: Sun, 24 May 2026 14:51:45 +0800 Subject: [PATCH 11/14] =?UTF-8?q?=E8=A1=A5=E5=85=85=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/mod/modrinth/ModrinthCompletionTask.java | 10 +--------- .../hmcl/mod/modrinth/ModrinthInstallTask.java | 9 +++++++++ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java index 5c3d40936b1..c6f06fc2033 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java @@ -19,11 +19,9 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.game.DefaultGameRepository; -import org.jackhuang.hmcl.mod.ModManager; import org.jackhuang.hmcl.mod.ModpackCompletionException; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.util.DigestUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.FileUtils; @@ -43,7 +41,6 @@ public class ModrinthCompletionTask extends Task { private final DefaultDependencyManager dependency; private final DefaultGameRepository repository; - private final ModManager modManager; private final String version; private ModrinthManifest manifest; private final List> dependencies = new ArrayList<>(); @@ -72,7 +69,6 @@ public ModrinthCompletionTask(DefaultDependencyManager dependencyManager, String public ModrinthCompletionTask(DefaultDependencyManager dependencyManager, String version, ModrinthManifest manifest) { this.dependency = dependencyManager; this.repository = dependencyManager.getGameRepository(); - this.modManager = repository.getModManager(version); this.version = version; this.manifest = manifest; @@ -104,7 +100,6 @@ public void execute() throws Exception { return; Path runDirectory = FileUtils.toAbsolute(repository.getRunDirectory(version)); - Path modsDirectory = runDirectory.resolve("mods"); for (ModrinthManifest.File file : manifest.getFiles()) { if (file.getEnv() != null && file.getEnv().getOrDefault("client", "required").equals("unsupported")) @@ -116,10 +111,7 @@ public void execute() throws Exception { if (!filePath.startsWith(runDirectory)) throw new IOException("Unsecure path: " + file.getPath()); - String sha1 = file.getHashes() != null ? file.getHashes().get("sha1") : null; - if (sha1 != null && Files.exists(filePath) && DigestUtils.digestToString("SHA-1", filePath).equalsIgnoreCase(sha1)) - continue; - if (modsDirectory.equals(filePath.getParent()) && this.modManager.hasSimpleMod(FileUtils.getName(filePath))) + if (Files.exists(filePath)) continue; var task = new FileDownloadTask( diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthInstallTask.java index 7059c77f979..d3e143cb730 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthInstallTask.java @@ -153,6 +153,15 @@ public void execute() throws Exception { Files.deleteIfExists(oldFile); } } + // For local files which is already in new manifest, remove them + for (ModrinthManifest.File newManifestFile : manifest.getFiles()) { + if (config.getManifest().getFiles().stream().noneMatch(newManifestFile::equals)) { + Path newFile = run.resolve(newManifestFile.getPath()); + if (Files.exists(newFile)) { + Files.deleteIfExists(newFile); + } + } + } } Path root = repository.getVersionRoot(name); From 8ddc5287d36a30734ca51436eb0a9f4dbac50dc1 Mon Sep 17 00:00:00 2001 From: link-fgfgui Date: Sun, 24 May 2026 16:40:32 +0800 Subject: [PATCH 12/14] =?UTF-8?q?=E8=A1=A5=E5=85=85javadoc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ModrinthCheckServerPackUpdateTask.java | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCheckServerPackUpdateTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCheckServerPackUpdateTask.java index 59843349a08..247b8371258 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCheckServerPackUpdateTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCheckServerPackUpdateTask.java @@ -33,19 +33,43 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; -/// Checks whether a Modrinth modpack with `fileApi` has a newer version available on the remote server. -/// -/// The result is `true` if an update is available, `false` otherwise. When an update is found, -/// the downloaded `server.mrpack` path and remote version id are stored in static fields and can -/// be retrieved via [getPendingUpdateFile] and [getPendingUpdateVersionId]. -/// Call [consumePendingUpdate] to retrieve and clear the pending update info. +/** + * Checks whether a Modrinth server modpack has a newer version available on the remote server. + * + * The task downloads {@code server.mrpack} from the remote API, extracts the manifest, and compares + * the remote version ID with the local one using {@link VersionNumber}. The result is {@code true} if + * an update is available, {@code false} otherwise. + * + * When an update is found, the downloaded {@code server.mrpack} path is stored in a static field + * and can be retrieved via {@link #getUpdateFile()}. If no update is available or an error occurs, + * the temporary file is deleted. + * + * @see ModrinthManifest + * @see FileDownloadTask + */ public class ModrinthCheckServerPackUpdateTask extends Task { + /** + * The path to the downloaded {@code server.mrpack} file, or {@code null} if no update file is available. + */ private static @Nullable Path updateFilePath = null; + /** + * The current local version ID of the modpack. + */ private final String versionId; - private final FileDownloadTask downloadTask; + /** + * The task that downloads {@code server.mrpack} from the remote API, or {@code null} if no download URL is available. + */ + private final @Nullable FileDownloadTask downloadTask; + + /** + * Creates a new update check task. + * + * @param versionId the current local version ID of the modpack. + * @param fileApi the base URL for downloading the server modpack; if blank, no download will be attempted. + */ public ModrinthCheckServerPackUpdateTask(String versionId, String fileApi) { this.versionId = versionId; @@ -71,6 +95,11 @@ public boolean isRelyingOnDependents() { return false; } + /** + * Returns the path to the downloaded {@code server.mrpack} file, or {@code null} if no update file is available. + * + * @return the path to the downloaded update file, or {@code null} + */ public static @Nullable Path getUpdateFile() { return updateFilePath; } From 9e93f131bc4241d6f4eac09d0b140f0ab4caf9ec Mon Sep 17 00:00:00 2001 From: link-fgfgui Date: Sun, 24 May 2026 16:42:34 +0800 Subject: [PATCH 13/14] =?UTF-8?q?=E6=81=A2=E5=A4=8D=E7=A6=81=E7=94=A8mod?= =?UTF-8?q?=E7=9A=84=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java index c6f06fc2033..682690c3656 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java @@ -19,6 +19,7 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.game.DefaultGameRepository; +import org.jackhuang.hmcl.mod.ModManager; import org.jackhuang.hmcl.mod.ModpackCompletionException; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Task; @@ -41,6 +42,7 @@ public class ModrinthCompletionTask extends Task { private final DefaultDependencyManager dependency; private final DefaultGameRepository repository; + private final ModManager modManager; private final String version; private ModrinthManifest manifest; private final List> dependencies = new ArrayList<>(); @@ -69,6 +71,7 @@ public ModrinthCompletionTask(DefaultDependencyManager dependencyManager, String public ModrinthCompletionTask(DefaultDependencyManager dependencyManager, String version, ModrinthManifest manifest) { this.dependency = dependencyManager; this.repository = dependencyManager.getGameRepository(); + this.modManager = repository.getModManager(version); this.version = version; this.manifest = manifest; @@ -100,6 +103,7 @@ public void execute() throws Exception { return; Path runDirectory = FileUtils.toAbsolute(repository.getRunDirectory(version)); + Path modsDirectory = runDirectory.resolve("mods"); for (ModrinthManifest.File file : manifest.getFiles()) { if (file.getEnv() != null && file.getEnv().getOrDefault("client", "required").equals("unsupported")) @@ -113,6 +117,8 @@ public void execute() throws Exception { if (Files.exists(filePath)) continue; + if (modsDirectory.equals(filePath.getParent()) && this.modManager.hasSimpleMod(FileUtils.getName(filePath))) + continue; var task = new FileDownloadTask( dependency.getDownloadProvider().injectURLsWithCandidates(file.getDownloads()), From 750a2f5466f554c81fc5b59f841150788114af15 Mon Sep 17 00:00:00 2001 From: link-fgfgui Date: Sun, 24 May 2026 17:16:55 +0800 Subject: [PATCH 14/14] fix checkstyleMain --- .../modrinth/ModrinthModpackExportTask.java | 78 +++++++++---------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index 4b8aa7b34f4..70db485f927 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -142,49 +142,49 @@ public void execute() throws Exception { String fileApi = StringUtils.isBlank(info.getFileApi()) ? null : StringUtils.removeSuffix(info.getFileApi(), "/"); try (Stream stream = Files.list(runDirectory)) { for (Path dirPath : (Iterable) stream::iterator) { - if (Files.exists(dirPath)) { - boolean isValidDir = Arrays.asList(resourceDirs).contains(dirPath.getFileName().toString()); // allow remote file match - Files.walk(dirPath) - .filter(Files::isRegularFile) - .forEach(file -> { - try { - String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/'); - - if (!info.getWhitelist().contains(relativePath)) { - return; - } + if (Files.exists(dirPath)) { + boolean isValidDir = Arrays.asList(resourceDirs).contains(dirPath.getFileName().toString()); // allow remote file match + Files.walk(dirPath) + .filter(Files::isRegularFile) + .forEach(file -> { + try { + String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/'); + + if (!info.getWhitelist().contains(relativePath)) { + return; + } - ModrinthManifest.File fileEntry = null; - if (isValidDir){ - fileEntry = tryGetRemoteFile(file, relativePath); - } - if (fileEntry != null) { - files.add(fileEntry); - filesInManifest.add(relativePath); - } else { - if (fileApi != null) { - Map hashes = new HashMap<>(); - hashes.put("sha1", DigestUtils.digestToString("SHA-1", file)); - hashes.put("sha512", DigestUtils.digestToString("SHA-512", file)); - - long fileSize = Files.size(file); - if (fileSize > Integer.MAX_VALUE) { - LOG.warning("File " + relativePath + " is too large (size: " + fileSize + " bytes), precision may be lost when converting to int"); + ModrinthManifest.File fileEntry = null; + if (isValidDir) { + fileEntry = tryGetRemoteFile(file, relativePath); + } + if (fileEntry != null) { + files.add(fileEntry); + filesInManifest.add(relativePath); + } else { + if (fileApi != null) { + Map hashes = new HashMap<>(); + hashes.put("sha1", DigestUtils.digestToString("SHA-1", file)); + hashes.put("sha512", DigestUtils.digestToString("SHA-512", file)); + + long fileSize = Files.size(file); + if (fileSize > Integer.MAX_VALUE) { + LOG.warning("File " + relativePath + " is too large (size: " + fileSize + " bytes), precision may be lost when converting to int"); + } + files.add(new ModrinthManifest.File( + relativePath, + hashes, + null, + Collections.singletonList(fileApi + "/" + NetworkUtils.encodeLocation(relativePath)), + (int) fileSize + )); } - files.add(new ModrinthManifest.File( - relativePath, - hashes, - null, - Collections.singletonList(fileApi + "/" + NetworkUtils.encodeLocation(relativePath)), - (int) fileSize - )); } + } catch (IOException e) { + LOG.warning("Failed to process file: " + file, e); } - } catch (IOException e) { - LOG.warning("Failed to process file: " + file, e); - } - }); - } + }); + } } }