From b6a3fe536b935b9c84a2b10f937ce7ce69bf6051 Mon Sep 17 00:00:00 2001 From: ONEDAY <62494140+fatelove42@users.noreply.github.com> Date: Mon, 3 Aug 2026 13:40:35 +0800 Subject: [PATCH 1/4] feat(modpack): add API retries and enable file hash caching for CurseForge completion --- .../modpack/curse/CurseCompletionTask.java | 53 +++++++++++++++---- .../hmcl/modpack/curse/CurseManifestFile.java | 30 +++++++++-- .../server/ServerModpackCompletionTask.java | 2 +- 3 files changed, 70 insertions(+), 15 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseCompletionTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseCompletionTask.java index d48570f82a4..dea908095ef 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseCompletionTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseCompletionTask.java @@ -122,15 +122,32 @@ public void execute() throws Exception { .map(file -> { updateProgress(finished.incrementAndGet(), manifest.files().size()); if (StringUtils.isBlank(file.fileName()) || file.url() == null) { - try { - RemoteAddon.File remoteFile = CurseForgeRemoteAddonRepository.MODS.getAddonFile(Integer.toString(file.projectID()), Integer.toString(file.fileID())); - return file.withFileName(remoteFile.filename()).withURL(remoteFile.url()); - } catch (FileNotFoundException fof) { - LOG.warning("Could not query api.curseforge.com for deleted mods: " + file.projectID() + ", " + file.fileID(), fof); - notFound.set(true); - return file; - } catch (IOException | JsonParseException e) { - LOG.warning("Unable to fetch the file name projectID=" + file.projectID() + ", fileID=" + file.fileID(), e); + RemoteAddon.File remoteFile = null; + Exception lastException = null; + for (int attempt = 0; attempt < 3; attempt++) { + try { + remoteFile = CurseForgeRemoteAddonRepository.MODS.getAddonFile(Integer.toString(file.projectID()), Integer.toString(file.fileID())); + break; + } catch (FileNotFoundException fof) { + LOG.warning("Could not query api.curseforge.com for deleted mods: " + file.projectID() + ", " + file.fileID(), fof); + notFound.set(true); + return file; + } catch (IOException | JsonParseException e) { + lastException = e; + if (attempt < 2) { + try { + Thread.sleep(500L * (attempt + 1)); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + break; + } + } + } + } + if (remoteFile != null) { + return file.withFileName(remoteFile.filename()).withURL(remoteFile.url()).withHashes(remoteFile.hashes()); + } else { + LOG.warning("Unable to fetch the file name projectID=" + file.projectID() + ", fileID=" + file.fileID(), lastException); allNameKnown.set(false); return file; } @@ -150,12 +167,26 @@ public void execute() throws Exception { .filter(f -> f.fileName() != null) .flatMap(f -> { try { - Path path = guessFilePath(f, dependency.getDownloadProvider(), resourcePacksRoot, shaderPacksRoot); + Path path = null; + for (int attempt = 0; attempt < 3; attempt++) { + try { + path = guessFilePath(f, dependency.getDownloadProvider(), resourcePacksRoot, shaderPacksRoot); + break; + } catch (IOException e) { + if (attempt == 2) throw e; + try { + Thread.sleep(500L * (attempt + 1)); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + break; + } + } + } if (path == null) { return Stream.empty(); } - var task = new FileDownloadTask(f.url(), path); + var task = new FileDownloadTask(List.of(NetworkUtils.toURI(f.url())), path, f.getIntegrityCheck()); task.setCacheRepository(dependency.getCacheRepository()); task.setCaching(true); return Stream.of(task.withCounter("hmcl.modpack.download")); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseManifestFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseManifestFile.java index b8edc77f56f..bd86bdb91cc 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseManifestFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseManifestFile.java @@ -31,7 +31,12 @@ public record CurseManifestFile(@SerializedName("projectID") int projectID, @SerializedName("fileID") int fileID, @SerializedName("fileName") String fileName, @SerializedName("url") String url, - @SerializedName("required") boolean required) implements Validation { + @SerializedName("required") boolean required, + @SerializedName("hashes") java.util.Map hashes) implements Validation { + + public CurseManifestFile(int projectID, int fileID, String fileName, String url, boolean required) { + this(projectID, fileID, fileName, url, required, null); + } @Override public void validate() throws JsonParseException { @@ -51,12 +56,31 @@ public String url() { } } + @Nullable + public org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck getIntegrityCheck() { + if (hashes == null || hashes.isEmpty()) return null; + if (hashes.containsKey("sha1")) { + return new org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck("SHA-1", hashes.get("sha1")); + } else if (hashes.containsKey("md5")) { + return new org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck("MD5", hashes.get("md5")); + } else if (hashes.containsKey("sha256")) { + return new org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck("SHA-256", hashes.get("sha256")); + } else if (hashes.containsKey("sha512")) { + return new org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck("SHA-512", hashes.get("sha512")); + } + return null; + } + public CurseManifestFile withFileName(String fileName) { - return new CurseManifestFile(projectID, fileID, fileName, url, required); + return new CurseManifestFile(projectID, fileID, fileName, url, required, hashes); } public CurseManifestFile withURL(String url) { - return new CurseManifestFile(projectID, fileID, fileName, url, required); + return new CurseManifestFile(projectID, fileID, fileName, url, required, hashes); + } + + public CurseManifestFile withHashes(java.util.Map hashes) { + return new CurseManifestFile(projectID, fileID, fileName, url, required, hashes); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackCompletionTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackCompletionTask.java index 2caca9ab70e..64f57a1082a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackCompletionTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackCompletionTask.java @@ -121,7 +121,7 @@ public void execute() throws Exception { dependencies.add(builder.buildAsync()); } - Path rootPath = repository.getInstanceRoot(instanceId).toAbsolutePath().normalize(); + Path rootPath = repository.getRunDirectory(instanceId).toAbsolutePath().normalize(); Map files = manifest.getManifest().getFiles().stream() .collect(Collectors.toMap(ModpackConfiguration.FileInformation::getPath, Function.identity())); From 6ba2bcc270ec9d15f9303e344307c7abebbc191f Mon Sep 17 00:00:00 2001 From: ONEDAY <62494140+fatelove42@users.noreply.github.com> Date: Mon, 3 Aug 2026 14:09:16 +0800 Subject: [PATCH 2/4] fix: use FileDownloadTask constructor in CurseCompletionTask --- .../org/jackhuang/hmcl/modpack/curse/CurseCompletionTask.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseCompletionTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseCompletionTask.java index dea908095ef..3671a0a46bf 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseCompletionTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseCompletionTask.java @@ -186,7 +186,7 @@ public void execute() throws Exception { return Stream.empty(); } - var task = new FileDownloadTask(List.of(NetworkUtils.toURI(f.url())), path, f.getIntegrityCheck()); + var task = new FileDownloadTask(f.url(), path, f.getIntegrityCheck()); task.setCacheRepository(dependency.getCacheRepository()); task.setCaching(true); return Stream.of(task.withCounter("hmcl.modpack.download")); From ad54d3a6370a8899709915c1cc09326a735c10a8 Mon Sep 17 00:00:00 2001 From: ONEDAY <62494140+fatelove42@users.noreply.github.com> Date: Mon, 3 Aug 2026 14:51:11 +0800 Subject: [PATCH 3/4] fix(curse): parse and preserve file hashes from CurseForge API in RemoteAddon.File --- .../repository/CurseForgeRemoteAddonRepository.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java index db6c0eb3da1..5f9d72bbd08 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java @@ -498,7 +498,15 @@ public RemoteAddon.Version toVersion() { null, fileDate(), versionType, - new RemoteAddon.File(Collections.emptyMap(), downloadUrl(), fileName()), + new RemoteAddon.File(hashes == null ? Collections.emptyMap() : hashes.stream().collect(Collectors.toMap( + hash -> switch (hash.algo()) { + case 1 -> "sha1"; + case 2 -> "md5"; + default -> "algo" + hash.algo(); + }, + LatestFileHash::value, + (a, b) -> a + )), downloadUrl(), fileName()), dependencies.stream().map(dependency -> { if (!RELATION_TYPE.containsKey(dependency.relationType())) { throw new IllegalStateException("Broken datas."); From 273baf1140b0b533455837cc61ecc2a4366f9297 Mon Sep 17 00:00:00 2001 From: ONEDAY <62494140+fatelove42@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:30:18 +0800 Subject: [PATCH 4/4] fix(curse): fetch hashes if file.hashes() is empty in CurseCompletionTask --- .../org/jackhuang/hmcl/modpack/curse/CurseCompletionTask.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseCompletionTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseCompletionTask.java index 3671a0a46bf..b26f282599e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseCompletionTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseCompletionTask.java @@ -121,7 +121,7 @@ public void execute() throws Exception { manifest.files().parallelStream() .map(file -> { updateProgress(finished.incrementAndGet(), manifest.files().size()); - if (StringUtils.isBlank(file.fileName()) || file.url() == null) { + if (StringUtils.isBlank(file.fileName()) || file.url() == null || file.hashes() == null || file.hashes().isEmpty()) { RemoteAddon.File remoteFile = null; Exception lastException = null; for (int attempt = 0; attempt < 3; attempt++) {