From b620bda89cf606705fd449f1a34251e6c48ca3c9 Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 16 Mar 2026 21:42:06 +0800 Subject: [PATCH 01/13] update --- .../org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java | 2 +- 1 file changed, 1 insertion(+), 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 452a114dab..3c6ffd4a44 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 @@ -123,7 +123,7 @@ public void execute() throws Exception { continue; var task = new FileDownloadTask( - file.getDownloads().stream().map(NetworkUtils::toURI).collect(Collectors.toList()), + dependency.getDownloadProvider().injectURLsWithCandidates(file.getDownloads()), filePath); task.setCacheRepository(dependency.getCacheRepository()); task.setCaching(true); From f40a4e94677566c0f967f0430260cad0878fc16d Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 16 Mar 2026 21:49:46 +0800 Subject: [PATCH 02/13] Add getDownloadProvider() getter method --- .../java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java index c0f668be6e..61bd6f9562 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java @@ -101,6 +101,10 @@ public DownloadListPage(RemoteModRepository repository, DownloadPage.DownloadCal this.downloadProvider = DownloadProviders.getDownloadProvider(); } + public DownloadProvider getDownloadProvider() { + return downloadProvider; + } + public ObservableList getActions() { return actions; } From f82331502d8292c023d68630921b5454e8c0ee0b Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 16 Mar 2026 21:54:20 +0800 Subject: [PATCH 03/13] Pass DownloadProvider to download callbacks --- .../jackhuang/hmcl/ui/download/DownloadPage.java | 14 +++++++------- .../jackhuang/hmcl/ui/versions/DownloadPage.java | 6 ++++-- .../hmcl/ui/versions/ModListPageSkin.java | 2 +- .../org/jackhuang/hmcl/ui/versions/Versions.java | 11 ++++++----- .../hmcl/mod/modrinth/ModrinthCompletionTask.java | 2 -- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index ff3f237f80..5c7996dea7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -84,8 +84,8 @@ public DownloadPage(String uploadVersion) { newGameTab.setNodeSupplier(loadVersionFor(() -> new VersionsPage(versionPageNavigator, i18n("install.installer.choose", i18n("install.installer.game")), "", DownloadProviders.getDownloadProvider(), "game", versionPageNavigator::onGameSelected))); modpackTab.setNodeSupplier(loadVersionFor(() -> { - DownloadListPage page = HMCLLocalizedDownloadListPage.ofModPack((profile, __, mod, file) -> { - Versions.downloadModpackImpl(profile, uploadVersion, mod, file); + DownloadListPage page = HMCLLocalizedDownloadListPage.ofModPack((downloadProvider, profile, __, mod, file) -> { + Versions.downloadModpackImpl(downloadProvider, profile, uploadVersion, mod, file); }, false); JFXButton installLocalModpackButton = FXUtils.newRaisedButton(i18n("install.modpack")); @@ -94,9 +94,9 @@ public DownloadPage(String uploadVersion) { page.getActions().add(installLocalModpackButton); return page; })); - modTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofMod((profile, version, mod, file) -> download(profile, version, file, "mods"), true))); - resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack((profile, version, mod, file) -> download(profile, version, file, "resourcepacks"), true))); - shaderTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofShaderPack((profile, version, mod, file) -> download(profile, version, file, "shaderpacks"), true))); + modTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofMod((downloadProvider, profile, version, mod, file) -> download(downloadProvider, profile, version, file, "mods"), true))); + resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack((downloadProvider, profile, version, mod, file) -> download(downloadProvider, profile, version, file, "resourcepacks"), true))); + shaderTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofShaderPack((downloadProvider, profile, version, mod, file) -> download(downloadProvider, profile, version, file, "shaderpacks"), true))); worldTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.WORLDS))); tab = new TabHeader(transitionPane, newGameTab, modpackTab, modTab, resourcePackTab, shaderTab, worldTab); @@ -129,7 +129,7 @@ private static Supplier loadVersionFor(Supplier nodeSuppl }; } - public static void download(Profile profile, @Nullable String version, RemoteMod.Version file, String subdirectoryName) { + public static void download(DownloadProvider downloadProvider, Profile profile, @Nullable String version, RemoteMod.Version file, String subdirectoryName) { if (version == null) version = profile.getSelectedVersion(); Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version) : profile.getRepository().getBaseDirectory(); @@ -138,7 +138,7 @@ public static void download(Profile profile, @Nullable String version, RemoteMod Path dest = runDirectory.resolve(subdirectoryName).resolve(result); Controllers.taskDialog(Task.composeAsync(() -> { - var task = new FileDownloadTask(file.getFile().getUrl(), dest); + var task = new FileDownloadTask(downloadProvider.injectURLWithCandidates(file.getFile().getUrl()), dest); task.setName(file.getName()); return task; }).whenComplete(Schedulers.javafx(), exception -> { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 856d87c641..aa913182c8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -38,6 +38,7 @@ import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.stage.FileChooser; +import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.game.Version; @@ -179,7 +180,7 @@ public void download(RemoteMod mod, RemoteMod.Version file) { if (this.callback == null) { saveAs(mod, file); } else { - this.callback.download(version.getProfile(), version.getVersion(), mod, file); + this.callback.download(page.getDownloadProvider(), version.getProfile(), version.getVersion(), mod, file); } } @@ -576,7 +577,8 @@ private void loadDependencies(RemoteMod.Version version, DownloadPage selfPage, } } + @FunctionalInterface public interface DownloadCallback { - void download(Profile profile, @Nullable String version, RemoteMod mod, RemoteMod.Version file); + void download(DownloadProvider downloadProvider, Profile profile, @Nullable String version, RemoteMod mod, RemoteMod.Version file); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java index a2b8adc6ef..a15d73982f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java @@ -493,7 +493,7 @@ final class ModInfoDialog extends JFXDialogLayout { repository instanceof CurseForgeRemoteModRepository ? HMCLLocalizedDownloadListPage.ofCurseForgeMod(null, false) : HMCLLocalizedDownloadListPage.ofModrinthMod(null, false), remoteMod, new Profile.ProfileVersion(ModListPageSkin.this.getSkinnable().getProfile(), ModListPageSkin.this.getSkinnable().getInstanceId()), - (profile, version, mod, file) -> org.jackhuang.hmcl.ui.download.DownloadPage.download(profile, version, file, "mods") + (downloadProvider, profile, version, mod, file) -> org.jackhuang.hmcl.ui.download.DownloadPage.download(downloadProvider, profile, version, file, "mods") )); }); button.setDisable(false); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java index d3b561fa9c..ae72ab4854 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java @@ -22,6 +22,7 @@ import javafx.stage.FileChooser; import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; +import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.download.game.GameAssetDownloadTask; import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.mod.RemoteMod; @@ -42,13 +43,13 @@ import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.TaskCancellationAction; import org.jackhuang.hmcl.util.io.FileUtils; -import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.platform.OperatingSystem; import java.io.IOException; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; @@ -72,11 +73,11 @@ public static void importModpack() { } } - public static void downloadModpackImpl(Profile profile, String version, RemoteMod mod, RemoteMod.Version file) { + public static void downloadModpackImpl(DownloadProvider downloadProvider, Profile profile, String version, RemoteMod mod, RemoteMod.Version file) { Path modpack; - URI downloadURL; + List downloadURLs; try { - downloadURL = NetworkUtils.toURI(file.getFile().getUrl()); + downloadURLs = downloadProvider.injectURLWithCandidates(file.getFile().getUrl()); modpack = Files.createTempFile("modpack", ".zip"); } catch (IOException | IllegalArgumentException e) { Controllers.dialog( @@ -85,7 +86,7 @@ public static void downloadModpackImpl(Profile profile, String version, RemoteMo return; } Controllers.taskDialog( - new FileDownloadTask(downloadURL, modpack) + new FileDownloadTask(downloadURLs, modpack) .whenComplete(Schedulers.javafx(), e -> { if (e == null) { ModpackInstallWizardProvider installWizardProvider; 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 3c6ffd4a44..682690c365 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 @@ -25,7 +25,6 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.FileUtils; -import org.jackhuang.hmcl.util.io.NetworkUtils; import java.io.FileNotFoundException; import java.io.IOException; @@ -36,7 +35,6 @@ import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; import static org.jackhuang.hmcl.util.logging.Logger.LOG; From 834ffe12c3239f5142d5ce206c1a95b9c7f53151 Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 16 Mar 2026 22:09:59 +0800 Subject: [PATCH 04/13] update --- .../download/BMCLAPIDownloadProvider.java | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java index ccafef0293..640b5a3d3c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java @@ -33,7 +33,6 @@ import org.jackhuang.hmcl.util.io.NetworkUtils; import java.net.URI; -import java.util.Arrays; import java.util.List; import static org.jackhuang.hmcl.util.Pair.pair; @@ -57,6 +56,7 @@ public final class BMCLAPIDownloadProvider implements DownloadProvider { private final QuiltVersionList quilt; private final QuiltAPIVersionList quiltApi; private final List> replacement; + private final List> fallbackReplacement; public BMCLAPIDownloadProvider(String apiRoot) { this.apiRoot = apiRoot; @@ -73,7 +73,7 @@ public BMCLAPIDownloadProvider(String apiRoot) { this.legacyFabric = new LegacyFabricVersionList(this); this.legacyFabricApi = new LegacyFabricAPIVersionList(this); - this.replacement = Arrays.asList( + this.replacement = List.of( pair("https://bmclapi2.bangbang93.com", apiRoot), pair("https://launchermeta.mojang.com", apiRoot), pair("https://piston-meta.mojang.com", apiRoot), @@ -93,7 +93,10 @@ public BMCLAPIDownloadProvider(String apiRoot) { pair("https://repo.maven.apache.org/maven2", "https://mirrors.cloud.tencent.com/nexus/repository/maven-public"), pair("https://hmcl.glavo.site/metadata/cleanroom", "https://alist.8mi.tech/d/mirror/HMCL-Metadata/Auto/cleanroom"), pair("https://hmcl.glavo.site/metadata/fmllibs", "https://alist.8mi.tech/d/mirror/HMCL-Metadata/Auto/fmllibs"), - pair("https://zkitefly.github.io/unlisted-versions-of-minecraft", "https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto"), + pair("https://zkitefly.github.io/unlisted-versions-of-minecraft", "https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto") + ); + + this.fallbackReplacement = List.of( // https://github.com/mcmod-info-mirror/mcim-rust-api pair("https://api.modrinth.com", "https://mod.mcimirror.top/modrinth"), pair("https://cdn.modrinth.com", "https://mod.mcimirror.top"), @@ -135,17 +138,37 @@ public VersionList getVersionListById(String id) { }; } - @Override - public String injectURL(String baseURL) { + private static String injectURL(List> replacement, String baseURL) { for (Pair pair : replacement) { if (baseURL.startsWith(pair.getKey())) { return pair.getValue() + baseURL.substring(pair.getKey().length()); } } - return baseURL; } + @Override + public String injectURL(String baseURL) { + return injectURL(replacement, baseURL); + } + + public List injectURLWithCandidates(String baseURL) { + String injected = injectURL(replacement, baseURL); + if (injected.equals(baseURL)) { + String fallbackInjected = injectURL(fallbackReplacement, baseURL); + if (fallbackInjected.equals(baseURL)) { + return List.of(NetworkUtils.toURI(baseURL)); + } else { + return List.of( + NetworkUtils.toURI(baseURL), + NetworkUtils.toURI(fallbackInjected) + ); + } + } else { + return List.of(NetworkUtils.toURI(injected)); + } + } + @Override public int getConcurrency() { return Math.max(Runtime.getRuntime().availableProcessors() * 2, 6); From 36b28ec6f64933734d5d9230694871727f6780c5 Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 16 Mar 2026 22:16:32 +0800 Subject: [PATCH 05/13] Wrap IOException in Modrinth search error handling --- .../hmcl/mod/modrinth/ModrinthRemoteModRepository.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java index 3c0ff4318e..ee65632ae1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java @@ -126,13 +126,15 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion return new SearchResult(response.getHits().stream().map(ProjectSearchResult::toMod), (int) Math.ceil((double) response.totalHits / pageSize)); } catch (IOException e) { LOG.warning("Failed to search addons: " + candidate, e); + + IOException wrapper = new IOException("Failed to search addons: " + candidate, e); if (candidates.size() == 1) { - exception = e; + exception = wrapper; } else { if (exception == null) { exception = new IOException("Failed to search addons"); } - exception.addSuppressed(e); + exception.addSuppressed(wrapper); } } } From 927cc06b84a11899ad9d1f5c667440ae89703733 Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 16 Mar 2026 22:25:39 +0800 Subject: [PATCH 06/13] Pass DownloadProvider to remote mod methods --- .../game/LocalizedRemoteModRepository.java | 4 ++-- .../hmcl/ui/versions/DownloadPage.java | 2 +- .../hmcl/ui/versions/ModCheckUpdatesTask.java | 7 +++++-- .../hmcl/ui/versions/ModListPage.java | 3 ++- .../download/fabric/FabricAPIVersionList.java | 2 +- .../LegacyFabricAPIVersionList.java | 2 +- .../download/quilt/QuiltAPIVersionList.java | 2 +- .../org/jackhuang/hmcl/mod/LocalModFile.java | 5 +++-- .../java/org/jackhuang/hmcl/mod/RemoteMod.java | 9 +++++---- .../hmcl/mod/RemoteModRepository.java | 2 +- .../jackhuang/hmcl/mod/curse/CurseAddon.java | 7 ++++--- .../curse/CurseForgeRemoteModRepository.java | 2 +- .../modrinth/ModrinthRemoteModRepository.java | 18 +++++++++--------- 13 files changed, 36 insertions(+), 29 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java index 8a15898173..878c09f98d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java @@ -125,7 +125,7 @@ public RemoteMod.File getModFile(String modId, String fileId) throws IOException } @Override - public Stream getRemoteVersionsById(String id) throws IOException { - return getBackedRemoteModRepository().getRemoteVersionsById(id); + public Stream getRemoteVersionsById(DownloadProvider downloadProvider, String id) throws IOException { + return getBackedRemoteModRepository().getRemoteVersionsById(downloadProvider, id); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index aa913182c8..83a52ea90d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -113,7 +113,7 @@ private void loadModVersions() { setFailed(false); Task.supplyAsync(() -> { - Stream versions = addon.getData().loadVersions(repository); + Stream versions = addon.getData().loadVersions(repository, page.getDownloadProvider()); return sortVersions(versions); }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModCheckUpdatesTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModCheckUpdatesTask.java index 286ae0fdb1..1d9b4a0641 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModCheckUpdatesTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModCheckUpdatesTask.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl.ui.versions; +import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.mod.LocalModFile; import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.task.Schedulers; @@ -30,16 +31,18 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; public class ModCheckUpdatesTask extends Task> { + private final DownloadProvider downloadProvider; private final List> dependents; - public ModCheckUpdatesTask(String gameVersion, Collection mods) { + public ModCheckUpdatesTask(DownloadProvider downloadProvider, String gameVersion, Collection mods) { + this.downloadProvider = downloadProvider; dependents = mods.stream().map(mod -> Task.supplyAsync(Schedulers.io(), () -> { LocalModFile.ModUpdate candidate = null; for (RemoteMod.Type type : RemoteMod.Type.values()) { LocalModFile.ModUpdate update = null; try { - update = mod.checkUpdates(gameVersion, type.getRemoteModRepository()); + update = mod.checkUpdates(downloadProvider, gameVersion, type.getRemoteModRepository()); } catch (IOException e) { LOG.warning(String.format("Cannot check update for mod %s.", mod.getFileName()), e); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java index 3ecb866f16..3c738da352 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java @@ -28,6 +28,7 @@ import org.jackhuang.hmcl.mod.LocalModFile; import org.jackhuang.hmcl.mod.ModLoaderType; import org.jackhuang.hmcl.mod.ModManager; +import org.jackhuang.hmcl.setting.DownloadProviders; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -240,7 +241,7 @@ public void checkUpdates(Collection mods) { .composeAsync(() -> { Optional gameVersion = profile.getRepository().getGameVersion(instanceId); if (gameVersion.isPresent()) { - return new ModCheckUpdatesTask(gameVersion.get(), mods); + return new ModCheckUpdatesTask(DownloadProviders.getDownloadProvider(), gameVersion.get(), mods); } return null; }) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIVersionList.java index 8c092822e5..0fde60902b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIVersionList.java @@ -42,7 +42,7 @@ public boolean hasType() { @Override public Task refreshAsync() { return Task.runAsync(() -> { - for (RemoteMod.Version modVersion : Lang.toIterable(ModrinthRemoteModRepository.MODS.getRemoteVersionsById("P7dR8mSH"))) { + for (RemoteMod.Version modVersion : Lang.toIterable(ModrinthRemoteModRepository.MODS.getRemoteVersionsById(downloadProvider, "P7dR8mSH"))) { for (String gameVersion : modVersion.getGameVersions()) { versions.put(gameVersion, new FabricAPIRemoteVersion(gameVersion, modVersion.getVersion(), modVersion.getName(), modVersion.getDatePublished(), modVersion, Collections.singletonList(modVersion.getFile().getUrl()))); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricAPIVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricAPIVersionList.java index da21df24a5..d7b6588e43 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricAPIVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricAPIVersionList.java @@ -42,7 +42,7 @@ public boolean hasType() { @Override public Task refreshAsync() { return Task.runAsync(() -> { - for (RemoteMod.Version modVersion : Lang.toIterable(ModrinthRemoteModRepository.MODS.getRemoteVersionsById("legacy-fabric-api"))) { + for (RemoteMod.Version modVersion : Lang.toIterable(ModrinthRemoteModRepository.MODS.getRemoteVersionsById(downloadProvider, "legacy-fabric-api"))) { for (String gameVersion : modVersion.getGameVersions()) { versions.put(gameVersion, new LegacyFabricAPIRemoteVersion(gameVersion, modVersion.getVersion(), modVersion.getName(), modVersion.getDatePublished(), modVersion, Collections.singletonList(modVersion.getFile().getUrl()))); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIVersionList.java index 04fc0327ad..d0b23fa8cf 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIVersionList.java @@ -42,7 +42,7 @@ public boolean hasType() { @Override public Task refreshAsync() { return Task.runAsync(() -> { - for (RemoteMod.Version modVersion : Lang.toIterable(ModrinthRemoteModRepository.MODS.getRemoteVersionsById("qsl"))) { + for (RemoteMod.Version modVersion : Lang.toIterable(ModrinthRemoteModRepository.MODS.getRemoteVersionsById(downloadProvider, "qsl"))) { for (String gameVersion : modVersion.getGameVersions()) { versions.put(gameVersion, new QuiltAPIRemoteVersion(gameVersion, modVersion.getVersion(), modVersion.getName(), modVersion.getDatePublished(), modVersion, Collections.singletonList(modVersion.getFile().getUrl()))); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java index aa12e7302b..cb8b341ea1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java @@ -19,6 +19,7 @@ import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; +import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.util.io.FileUtils; import java.io.IOException; @@ -177,10 +178,10 @@ public void disable() throws IOException { file = modManager.disableMod(file); } - public ModUpdate checkUpdates(String gameVersion, RemoteModRepository repository) throws IOException { + public ModUpdate checkUpdates(DownloadProvider downloadProvider, String gameVersion, RemoteModRepository repository) throws IOException { Optional currentVersion = repository.getRemoteVersionByLocalFile(this, file); if (!currentVersion.isPresent()) return null; - List remoteVersions = repository.getRemoteVersionsById(currentVersion.get().getModid()) + List remoteVersions = repository.getRemoteVersionsById(downloadProvider, currentVersion.get().getModid()) .filter(version -> version.getGameVersions().contains(gameVersion)) .filter(version -> version.getLoaders().contains(getModLoaderType())) .filter(version -> version.getDatePublished().compareTo(currentVersion.get().getDatePublished()) > 0) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java index ce7c56e235..1d0cc94540 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl.mod; +import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository; import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository; import org.jackhuang.hmcl.task.FileDownloadTask; @@ -32,12 +33,12 @@ public final class RemoteMod { public static final RemoteMod BROKEN = new RemoteMod("", "", "RemoteMod.BROKEN", "", Collections.emptyList(), "", "", new RemoteMod.IMod() { @Override - public List loadDependencies(RemoteModRepository modRepository) throws IOException { + public List loadDependencies(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException { throw new IOException(); } @Override - public Stream loadVersions(RemoteModRepository modRepository) throws IOException { + public Stream loadVersions(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException { throw new IOException(); } }); @@ -202,9 +203,9 @@ public RemoteModRepository getRemoteModRepository() { } public interface IMod { - List loadDependencies(RemoteModRepository modRepository) throws IOException; + List loadDependencies(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException; - Stream loadVersions(RemoteModRepository modRepository) throws IOException; + Stream loadVersions(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException; } public interface IVersion { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java index 8325375eb0..7174de530c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java @@ -98,7 +98,7 @@ default RemoteMod resolveDependency(String id) throws IOException { RemoteMod.File getModFile(String modId, String fileId) throws IOException; - Stream getRemoteVersionsById(String id) throws IOException; + Stream getRemoteVersionsById(DownloadProvider downloadProvider, String id) throws IOException; Stream getCategories() throws IOException; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java index e77fb259e1..9b3d6887c4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl.mod.curse; +import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.mod.ModLoaderType; import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.RemoteModRepository; @@ -191,7 +192,7 @@ public int getThumbsUpCount() { } @Override - public List loadDependencies(RemoteModRepository modRepository) throws IOException { + public List loadDependencies(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException { Set dependencies = latestFiles.stream() .flatMap(latestFile -> latestFile.getDependencies().stream()) .filter(dep -> dep.getRelationType() == 3) @@ -205,8 +206,8 @@ public List loadDependencies(RemoteModRepository modRepository) throw } @Override - public Stream loadVersions(RemoteModRepository modRepository) throws IOException { - return modRepository.getRemoteVersionsById(Integer.toString(id)); + public Stream loadVersions(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException { + return modRepository.getRemoteVersionsById(downloadProvider, Integer.toString(id)); } public RemoteMod toMod() { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java index 0bb86b50ba..6f0eff3756 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java @@ -251,7 +251,7 @@ public RemoteMod.File getModFile(String modId, String fileId) throws IOException } @Override - public Stream getRemoteVersionsById(String id) throws IOException { + public Stream getRemoteVersionsById(DownloadProvider downloadProvider, String id) throws IOException { SEMAPHORE.acquireUninterruptibly(); try { Response> response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id + "/files", diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java index ee65632ae1..d1ba1a9f08 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java @@ -200,7 +200,7 @@ public RemoteMod.File getModFile(String modId, String fileId) throws IOException } @Override - public Stream getRemoteVersionsById(String id) throws IOException { + public Stream getRemoteVersionsById(DownloadProvider downloadProvider, String id) throws IOException { SEMAPHORE.acquireUninterruptibly(); try { id = StringUtils.removePrefix(id, "local-"); @@ -364,8 +364,8 @@ public List getVersions() { } @Override - public List loadDependencies(RemoteModRepository modRepository) throws IOException { - Set dependencies = modRepository.getRemoteVersionsById(getId()) + public List loadDependencies(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException { + Set dependencies = modRepository.getRemoteVersionsById(downloadProvider, getId()) .flatMap(version -> version.getDependencies().stream()) .collect(Collectors.toSet()); List mods = new ArrayList<>(); @@ -376,8 +376,8 @@ public List loadDependencies(RemoteModRepository modRepository) throw } @Override - public Stream loadVersions(RemoteModRepository modRepository) throws IOException { - return modRepository.getRemoteVersionsById(getId()); + public Stream loadVersions(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException { + return modRepository.getRemoteVersionsById(downloadProvider, getId()); } public RemoteMod toMod() { @@ -751,8 +751,8 @@ public String getLatestVersion() { } @Override - public List loadDependencies(RemoteModRepository modRepository) throws IOException { - Set dependencies = modRepository.getRemoteVersionsById(getProjectId()) + public List loadDependencies(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException { + Set dependencies = modRepository.getRemoteVersionsById(downloadProvider, getProjectId()) .flatMap(version -> version.getDependencies().stream()) .collect(Collectors.toSet()); List mods = new ArrayList<>(); @@ -763,8 +763,8 @@ public List loadDependencies(RemoteModRepository modRepository) throw } @Override - public Stream loadVersions(RemoteModRepository modRepository) throws IOException { - return modRepository.getRemoteVersionsById(getProjectId()); + public Stream loadVersions(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException { + return modRepository.getRemoteVersionsById(downloadProvider, getProjectId()); } public RemoteMod toMod() { From 80ba4ff253e7e8b391251e36a79afae47956ab4a Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 16 Mar 2026 22:29:30 +0800 Subject: [PATCH 07/13] Wrap IOException in Modrinth version fetching --- .../modrinth/ModrinthRemoteModRepository.java | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java index d1ba1a9f08..44a6deddf9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java @@ -204,9 +204,29 @@ public Stream getRemoteVersionsById(DownloadProvider download SEMAPHORE.acquireUninterruptibly(); try { id = StringUtils.removePrefix(id, "local-"); - List versions = HttpRequest.GET(PREFIX + "/v2/project/" + id + "/version?include_changelog=false") - .getJson(listTypeOf(ProjectVersion.class)); - return versions.stream().map(ProjectVersion::toVersion).flatMap(Lang::toStream); + + List candidates = downloadProvider.injectURLWithCandidates(PREFIX + "/v2/project/" + id + "/version?include_changelog=false"); + IOException exception = null; + + for (URI candidate : candidates) { + try { + List versions = HttpRequest.GET(candidate.toString()) + .getJson(listTypeOf(ProjectVersion.class)); + return versions.stream().map(ProjectVersion::toVersion).flatMap(Lang::toStream); + } catch (IOException e) { + IOException wrapper = new IOException("Failed to get remote versions: " + candidate, e); + if (candidates.size() == 1) { + exception = wrapper; + } else { + if (exception == null) { + exception = new IOException("Failed to get remote versions"); + } + exception.addSuppressed(wrapper); + } + } + } + + throw exception != null ? exception : new IOException("No candidates found"); } finally { SEMAPHORE.release(); } From dbb4035ca4f4a8dd04bbd6d9247eb8200d5312e2 Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 16 Mar 2026 22:37:25 +0800 Subject: [PATCH 08/13] Pass DownloadProvider to getModById and resolveDependency --- .../game/LocalizedRemoteModRepository.java | 4 +-- .../hmcl/ui/versions/DownloadPage.java | 2 +- .../hmcl/ui/versions/ModListPageSkin.java | 3 +- .../org/jackhuang/hmcl/mod/RemoteMod.java | 4 +-- .../hmcl/mod/RemoteModRepository.java | 6 ++-- .../jackhuang/hmcl/mod/curse/CurseAddon.java | 2 +- .../hmcl/mod/curse/CurseCompletionTask.java | 8 +++-- .../curse/CurseForgeRemoteModRepository.java | 2 +- .../modrinth/ModrinthRemoteModRepository.java | 34 ++++++++++++++----- 9 files changed, 43 insertions(+), 22 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java index 878c09f98d..29b8fb5a13 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java @@ -115,8 +115,8 @@ public Optional getRemoteVersionByLocalFile(LocalModFile loca } @Override - public RemoteMod getModById(String id) throws IOException { - return getBackedRemoteModRepository().getModById(id); + public RemoteMod getModById(DownloadProvider downloadProvider, String id) throws IOException { + return getBackedRemoteModRepository().getModById(downloadProvider, id); } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 83a52ea90d..ed2e4a8d42 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -549,7 +549,7 @@ private void loadDependencies(RemoteMod.Version version, DownloadPage selfPage, dependencies.put(dependency.getType(), list); } - queue.add(Task.supplyAsync(Schedulers.io(), dependency::load) + queue.add(Task.supplyAsync(Schedulers.io(), () -> dependency.load(selfPage.page.getDownloadProvider())) .setSignificance(Task.TaskSignificance.MINOR) .thenAcceptAsync(Schedulers.javafx(), dep -> { if (dep == RemoteMod.BROKEN) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java index a15d73982f..bad72233f5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java @@ -43,6 +43,7 @@ import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository; import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository; +import org.jackhuang.hmcl.setting.DownloadProviders; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.VersionIconType; import org.jackhuang.hmcl.task.Schedulers; @@ -465,7 +466,7 @@ final class ModInfoDialog extends JFXDialogLayout { Task.runAsync(() -> { Optional versionOptional = repository.getRemoteVersionByLocalFile(modInfo.getModInfo(), modInfo.getModInfo().getFile()); if (versionOptional.isPresent()) { - RemoteMod remoteMod = repository.getModById(versionOptional.get().getModid()); + RemoteMod remoteMod = repository.getModById(DownloadProviders.getDownloadProvider(), versionOptional.get().getModid()); FXUtils.runInFX(() -> { for (ModLoaderType modLoaderType : versionOptional.get().getLoaders()) { String loaderName = switch (modLoaderType) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java index 1d0cc94540..91628a7b50 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java @@ -155,12 +155,12 @@ public String getId() { return this.id; } - public RemoteMod load() throws IOException { + public RemoteMod load(DownloadProvider downloadProvider) throws IOException { if (this.remoteMod == null) { if (this.type == DependencyType.BROKEN) { this.remoteMod = RemoteMod.BROKEN; } else { - this.remoteMod = this.remoteModRepository.resolveDependency(this.id); + this.remoteMod = this.remoteModRepository.resolveDependency(downloadProvider, this.id); } } return this.remoteMod; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java index 7174de530c..6833d9170d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java @@ -90,10 +90,10 @@ SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Null Optional getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException; - RemoteMod getModById(String id) throws IOException; + RemoteMod getModById(DownloadProvider downloadProvider, String id) throws IOException; - default RemoteMod resolveDependency(String id) throws IOException { - return getModById(id); + default RemoteMod resolveDependency(DownloadProvider downloadProvider, String id) throws IOException { + return getModById(downloadProvider, id); } RemoteMod.File getModFile(String modId, String fileId) throws IOException; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java index 9b3d6887c4..32e67c16e9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java @@ -200,7 +200,7 @@ public List loadDependencies(RemoteModRepository modRepository, Downl .collect(Collectors.toSet()); List mods = new ArrayList<>(); for (int dependencyId : dependencies) { - mods.add(modRepository.getModById(Integer.toString(dependencyId))); + mods.add(modRepository.getModById(downloadProvider, Integer.toString(dependencyId))); } return mods; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseCompletionTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseCompletionTask.java index 90cffdcfdb..c767fa8902 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseCompletionTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseCompletionTask.java @@ -19,6 +19,7 @@ import com.google.gson.JsonParseException; import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.game.DefaultGameRepository; import org.jackhuang.hmcl.mod.ModManager; import org.jackhuang.hmcl.mod.ModpackCompletionException; @@ -147,7 +148,7 @@ public void execute() throws Exception { .filter(f -> f.fileName() != null) .flatMap(f -> { try { - Path path = guessFilePath(f, resourcePacksRoot, shaderPacksRoot); + Path path = guessFilePath(f, dependency.getDownloadProvider(), resourcePacksRoot, shaderPacksRoot); if (path == null) { return Stream.empty(); } @@ -175,13 +176,14 @@ public void execute() throws Exception { * Guess where to store the file. * * @param file The file. + * @param downloadProvider * @param resourcePacksRoot ./resourcepacks. * @param shaderPacksRoot ./shaderpacks. * @return ./resourcepacks/$filename or ./shaderpacks/$filename or ./mods/$filename if the file doesn't exist. null if the file existed. * @throws IOException If IOException was encountered during getting data from CurseForge. */ - private Path guessFilePath(CurseManifestFile file, Path resourcePacksRoot, Path shaderPacksRoot) throws IOException { - RemoteMod mod = CurseForgeRemoteModRepository.MODS.getModById(Integer.toString(file.projectID())); + private Path guessFilePath(CurseManifestFile file, DownloadProvider downloadProvider, Path resourcePacksRoot, Path shaderPacksRoot) throws IOException { + RemoteMod mod = CurseForgeRemoteModRepository.MODS.getModById(downloadProvider, Integer.toString(file.projectID())); int classID = ((CurseAddon) mod.getData()).getClassId(); String fileName = file.fileName(); return switch (classID) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java index 6f0eff3756..6921fbc479 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java @@ -227,7 +227,7 @@ public Optional getRemoteVersionByLocalFile(LocalModFile loca } @Override - public RemoteMod getModById(String id) throws IOException { + public RemoteMod getModById(DownloadProvider downloadProvider, String id) throws IOException { SEMAPHORE.acquireUninterruptibly(); try { Response response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id)) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java index 44a6deddf9..df6f5ac7e8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java @@ -115,7 +115,6 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion pair("index", convertSortType(sort)) ); - List candidates = downloadProvider.injectURLWithCandidates(NetworkUtils.withQuery(PREFIX + "/v2/search", query)); IOException exception = null; for (URI candidate : candidates) { @@ -169,21 +168,40 @@ public Optional getRemoteVersionByLocalFile(LocalModFile loca } @Override - public RemoteMod getModById(String id) throws IOException { + public RemoteMod getModById(DownloadProvider downloadProvider, String id) throws IOException { SEMAPHORE.acquireUninterruptibly(); try { id = StringUtils.removePrefix(id, "local-"); - Project project = HttpRequest.GET(PREFIX + "/v2/project/" + id).getJson(Project.class); - return project.toMod(); + List candidates = downloadProvider.injectURLWithCandidates(PREFIX + "/v2/project/" + id); + IOException exception = null; + + for (URI candidate : candidates) { + try { + Project project = HttpRequest.GET(candidate.toString()).getJson(Project.class); + return project.toMod(); + } catch (IOException e) { + IOException wrapper = new IOException("Failed to get mod: " + candidate, e); + if (candidates.size() == 1) { + exception = wrapper; + } else { + if (exception == null) { + exception = new IOException("Failed to get mod"); + } + exception.addSuppressed(wrapper); + } + } + } + + throw exception != null ? exception : new IOException("No candidates found"); } finally { SEMAPHORE.release(); } } @Override - public RemoteMod resolveDependency(String id) throws IOException { + public RemoteMod resolveDependency(DownloadProvider downloadProvider, String id) throws IOException { try { - return getModById(id); + return getModById(downloadProvider, id); } catch (ResponseCodeException e) { if (e.getResponseCode() == 502 || e.getResponseCode() == 404) { return RemoteMod.BROKEN; @@ -390,7 +408,7 @@ public List loadDependencies(RemoteModRepository modRepository, Downl .collect(Collectors.toSet()); List mods = new ArrayList<>(); for (RemoteMod.Dependency dependency : dependencies) { - mods.add(dependency.load()); + mods.add(dependency.load(downloadProvider)); } return mods; } @@ -777,7 +795,7 @@ public List loadDependencies(RemoteModRepository modRepository, Downl .collect(Collectors.toSet()); List mods = new ArrayList<>(); for (RemoteMod.Dependency dependency : dependencies) { - mods.add(dependency.load()); + mods.add(dependency.load(downloadProvider)); } return mods; } From 305146eea89deea3cf15ff7fd884b627b477d828 Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 16 Mar 2026 22:40:52 +0800 Subject: [PATCH 09/13] Add CacheFileTask constructor for URI list --- .../jackhuang/hmcl/mod/modrinth/ModrinthInstallTask.java | 3 ++- .../main/java/org/jackhuang/hmcl/task/CacheFileTask.java | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) 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 5a557008e6..5ac9443f5b 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 @@ -123,7 +123,8 @@ public ModrinthInstallTask(DefaultDependencyManager dependencyManager, Path zipF String ext = FileUtils.getExtension(StringUtils.substringAfter(iconUri.getPath(), '/')).toLowerCase(Locale.ROOT); if (SUPPORTED_ICON_EXTS.contains(ext)) { iconExt = ext; - dependents.add(downloadIconTask = new CacheFileTask(iconUrl)); + + dependents.add(downloadIconTask = new CacheFileTask(dependencyManager.getDownloadProvider().injectURLWithCandidates(iconUrl))); } } dependencies.add(new ModrinthCompletionTask(dependencyManager, name, manifest)); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/CacheFileTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/CacheFileTask.java index 9e472fa4ac..d361f039f4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/CacheFileTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/CacheFileTask.java @@ -50,6 +50,14 @@ public CacheFileTask(@NotNull URI uri) { throw new IllegalArgumentException(uri.toString()); } + public CacheFileTask(@NotNull List<@NotNull URI> uris) { + super(uris); + setName(uris.get(0).toString()); + + if (!uris.stream().allMatch(NetworkUtils::isHttpUri)) + throw new IllegalArgumentException(uris.toString()); + } + @Override protected EnumCheckETag shouldCheckETag() { // Check cache From 4efd16bb72ca21dbf4aff5e16e4d1c7acc4e7237 Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 16 Mar 2026 22:45:34 +0800 Subject: [PATCH 10/13] Pass DownloadProvider to RemoteImageLoader --- .../main/java/org/jackhuang/hmcl/ui/FXUtils.java | 2 +- .../hmcl/ui/versions/DownloadListPage.java | 14 ++++++++------ .../org/jackhuang/hmcl/util/RemoteImageLoader.java | 10 +++++++--- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index 41c6db9e5c..957b8bd121 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -1241,7 +1241,7 @@ public static Task getRemoteImageTask(String url, int requestedWidth, int .setSignificance(Task.TaskSignificance.MINOR); } - public static Task getRemoteImageTask(URI uri, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) { + public static Task getRemoteImageTask(List uri, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) { return new CacheFileTask(uri) .setSignificance(Task.TaskSignificance.MINOR) .thenApplyAsync(file -> loadImage(file, requestedWidth, requestedHeight, preserveRatio, smooth)) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java index 61bd6f9562..7de131b011 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java @@ -237,18 +237,20 @@ protected Skin createDefaultSkin() { private static class ModDownloadListPageSkin extends SkinBase { private final JFXListView listView = new JFXListView<>(); - private final RemoteImageLoader iconLoader = new RemoteImageLoader() { - @Override - protected @NotNull Task createLoadTask(@NotNull URI uri) { - return FXUtils.getRemoteImageTask(uri, 80, 80, true, true); - } - }; + private final RemoteImageLoader iconLoader; protected ModDownloadListPageSkin(DownloadListPage control) { super(control); listView.getStyleClass().add("no-horizontal-scrollbar"); + iconLoader = new RemoteImageLoader(control.downloadProvider) { + @Override + protected @NotNull Task createLoadTask(@NotNull List uris) { + return FXUtils.getRemoteImageTask(uris, 80, 80, true, true); + } + }; + BorderPane pane = new BorderPane(); GridPane searchPane = new GridPane(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/RemoteImageLoader.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/RemoteImageLoader.java index f36a1bdeab..4f7c78d0d1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/RemoteImageLoader.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/RemoteImageLoader.java @@ -19,6 +19,7 @@ import javafx.beans.value.WritableValue; import javafx.scene.image.Image; +import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.io.NetworkUtils; @@ -26,6 +27,7 @@ import org.jetbrains.annotations.Nullable; import java.lang.ref.WeakReference; +import java.net.DatagramPacket; import java.net.URI; import java.util.*; @@ -33,18 +35,20 @@ /// @author Glavo public abstract class RemoteImageLoader { + private final DownloadProvider downloadProvider; private final Map> cache = new HashMap<>(); private final Map>>> pendingRequests = new HashMap<>(); private final WeakHashMap, URI> reverseLookup = new WeakHashMap<>(); - public RemoteImageLoader() { + public RemoteImageLoader(DownloadProvider downloadProvider) { + this.downloadProvider = downloadProvider; } protected @Nullable Image getPlaceholder() { return null; } - protected abstract @NotNull Task createLoadTask(@NotNull URI uri); + protected abstract @NotNull Task createLoadTask(@NotNull List uris); @FXThread public void load(@NotNull WritableValue writableValue, String url) { @@ -82,7 +86,7 @@ public void load(@NotNull WritableValue writableValue, String url) { } } - createLoadTask(uri).whenComplete(Schedulers.javafx(), (result, exception) -> { + createLoadTask(downloadProvider.injectURLWithCandidates(url)).whenComplete(Schedulers.javafx(), (result, exception) -> { Image image; if (exception == null) { image = result; From 64e7e5b8eac9099055fc857022145daa3cc7547d Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 16 Mar 2026 22:45:57 +0800 Subject: [PATCH 11/13] Remove unused DatagramPacket import --- .../src/main/java/org/jackhuang/hmcl/util/RemoteImageLoader.java | 1 - 1 file changed, 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/RemoteImageLoader.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/RemoteImageLoader.java index 4f7c78d0d1..6cc4ee7aa9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/RemoteImageLoader.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/RemoteImageLoader.java @@ -27,7 +27,6 @@ import org.jetbrains.annotations.Nullable; import java.lang.ref.WeakReference; -import java.net.DatagramPacket; import java.net.URI; import java.util.*; From 3a3772aa49ac563c6de30930060cec67f8b57a20 Mon Sep 17 00:00:00 2001 From: Glavo Date: Tue, 17 Mar 2026 20:42:52 +0800 Subject: [PATCH 12/13] Rename uri parameter to uris in getRemoteImageTask --- HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index 957b8bd121..ca849833ed 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -1241,8 +1241,8 @@ public static Task getRemoteImageTask(String url, int requestedWidth, int .setSignificance(Task.TaskSignificance.MINOR); } - public static Task getRemoteImageTask(List uri, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) { - return new CacheFileTask(uri) + public static Task getRemoteImageTask(List uris, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) { + return new CacheFileTask(uris) .setSignificance(Task.TaskSignificance.MINOR) .thenApplyAsync(file -> loadImage(file, requestedWidth, requestedHeight, preserveRatio, smooth)) .setSignificance(Task.TaskSignificance.MINOR); From 473b213c665d103cab72755343068737efe55c87 Mon Sep 17 00:00:00 2001 From: Glavo Date: Tue, 17 Mar 2026 20:43:23 +0800 Subject: [PATCH 13/13] Add @Override annotation to injectURLWithCandidates --- .../org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java | 1 + 1 file changed, 1 insertion(+) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java index 640b5a3d3c..b23a7867f7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java @@ -152,6 +152,7 @@ public String injectURL(String baseURL) { return injectURL(replacement, baseURL); } + @Override public List injectURLWithCandidates(String baseURL) { String injected = injectURL(replacement, baseURL); if (injected.equals(baseURL)) {