Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ public RemoteAddon getAddonById(DownloadProvider downloadProvider, String id) th
return getBackedRemoteModRepository().getAddonById(downloadProvider, id);
}

@Override
public RemoteAddon resolveDependency(DownloadProvider downloadProvider, String id) throws IOException {
return getBackedRemoteModRepository().resolveDependency(downloadProvider, id);
}

@Override
public RemoteAddon.File getAddonFile(String projectId, String fileId) throws IOException {
return getBackedRemoteModRepository().getAddonFile(projectId, fileId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,21 +401,15 @@ private static final class DependencyAddonItem extends LineButton {
});
setNode(IDX_LEADING, pane);

if (addon != RemoteAddon.BROKEN) {
ModTranslations.Mod mod = ModTranslations.getTranslationsByAddonType(type).getModByCurseForgeId(addon.slug());
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : addon.title());
content.setSubtitle(addon.description());
for (String category : addon.categories()) {
if (page.shouldDisplayCategory(category))
content.addTag(page.getLocalizedCategory(category, null));
}
if (StringUtils.isNotBlank(addon.iconUrl())) {
imageView.imageProperty().bind(FXUtils.newRemoteImage(addon.iconUrl(), 80, 80, true, true));
}
} else {
content.setTitle(i18n("addon.broken_dependency.title"));
content.setSubtitle(i18n("addon.broken_dependency.desc"));
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/icon@4x.png"));
ModTranslations.Mod mod = ModTranslations.getTranslationsByAddonType(type).getModByCurseForgeId(addon.slug());
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : addon.title());
content.setSubtitle(addon.description());
for (String category : addon.categories()) {
if (page.shouldDisplayCategory(category))
content.addTag(page.getLocalizedCategory(category, null));
}
if (StringUtils.isNotBlank(addon.iconUrl())) {
imageView.imageProperty().bind(FXUtils.newRemoteImage(addon.iconUrl(), 80, 80, true, true));
}
}
}
Expand Down Expand Up @@ -549,6 +543,7 @@ public AddonVersion(RemoteAddon mod, RemoteAddon.Version version, DownloadPage s
scrollPane.setContent(dependenciesList);
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
FXUtils.onChangeAndOperate(scrollPane.widthProperty(), d -> FXUtils.setLimitWidth(dependenciesList, d.doubleValue()));
FXUtils.smoothScrolling(scrollPane);
FXUtils.setOverflowHidden(scrollPane, 8);
spinnerPane.setContent(scrollPane);
Expand Down Expand Up @@ -619,17 +614,19 @@ private void loadDependencies(RemoteAddon.Version version, DownloadPage selfPage
return;
}
DependencyAddonItem dependencyAddonItem = new DependencyAddonItem(selfPage.page, dep, selfPage.instanceReference);
FXUtils.onWeakChangeAndOperate(dependenciesList.widthProperty(), d -> FXUtils.setLimitWidth(dependencyAddonItem, d.doubleValue()));
dependencies.get(dependency.getType()).value().add(dependencyAddonItem);
})
.setSignificance(Task.TaskSignificance.MINOR));
}

return Task.allOf(queue).thenSupplyAsync(() ->
dependencies.values().stream().flatMap(types ->
Stream.concat(
Stream.of(types.key()),
types.value().stream().sorted(Comparator.comparing(item -> item.addon.slug(), String.CASE_INSENSITIVE_ORDER)))
).toList()
dependencies.values().stream().flatMap(types -> {
if (types.value().isEmpty()) return Stream.of();
return Stream.concat(
Stream.of(types.key()),
types.value().stream().sorted(Comparator.comparing(item -> item.addon.slug(), String.CASE_INSENSITIVE_ORDER)));
}).toList()
);
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
Expand Down
2 changes: 0 additions & 2 deletions HMCL/src/main/resources/assets/lang/I18N.properties
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,6 @@ account.skin.upload.failed=Failed to upload skin.
account.skin.invalid_skin=Invalid skin file.
account.username=Username

addon.broken_dependency.title=Broken dependency
addon.broken_dependency.desc=This dependency existed before, but it does not exist anymore. Try using another download source.
addon.category=Category
addon.changelog=Changelog
addon.changelog.empty=Currently no changelog
Expand Down
2 changes: 0 additions & 2 deletions HMCL/src/main/resources/assets/lang/I18N_ar.properties
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,6 @@ account.skin.upload.failed=فشل رفع المظهر.
account.skin.invalid_skin=ملف المظهر غير صالح.
account.username=اسم المستخدم

addon.broken_dependency.title=تبعية معطوبة
addon.broken_dependency.desc=كانت هذه التبعية موجودة مسبقاً، لكنها لم تعد كذلك. جرّب استخدام مصدر تنزيل آخر.
addon.category=الفئة
addon.channel.alpha=ألفا
addon.channel.beta=بيتا
Expand Down
2 changes: 0 additions & 2 deletions HMCL/src/main/resources/assets/lang/I18N_es.properties
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,6 @@ account.skin.upload.failed=No se pudo subir el aspecto.
account.skin.invalid_skin=Archivo de aspecto inválido.
account.username=Nombre de usuario

addon.broken_dependency.title=Dependencia rota
addon.broken_dependency.desc=Esta dependencia existía antes, pero ahora no existe. Intente utilizar otra fuente de descarga.
addon.category=Categoría
addon.channel.alpha=Alpha
addon.channel.beta=Beta
Expand Down
2 changes: 0 additions & 2 deletions HMCL/src/main/resources/assets/lang/I18N_lzh.properties
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,6 @@ account.skin.upload.failed=匯之外觀而未成
account.skin.invalid_skin=外觀之案有謬
account.username=戶名

addon.broken_dependency.title=所依之壞者
addon.broken_dependency.desc=夫改囊素存於改囊庫,今闕矣,宜易他源。
addon.category=類
addon.changelog=迭更誌
addon.channel.alpha=預版
Expand Down
2 changes: 0 additions & 2 deletions HMCL/src/main/resources/assets/lang/I18N_ru.properties
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,6 @@ account.skin.upload.failed=Не удалось загрузить скин.
account.skin.invalid_skin=Недопустимый файл скина.
account.username=Имя пользователя

addon.broken_dependency.title=Сломанная зависимость
addon.broken_dependency.desc=Эта зависимость существовала раньше, но теперь ее нет. Попробуйте использовать другой источник скачивания.
addon.category=Категория
addon.channel.alpha=Альфа
addon.channel.beta=Бета
Expand Down
2 changes: 0 additions & 2 deletions HMCL/src/main/resources/assets/lang/I18N_uk.properties
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,6 @@ account.skin.upload.failed=Не вдалося завантажити скін.
account.skin.invalid_skin=Недійсний файл скіна.
account.username=Ім'я користувача

addon.broken_dependency.title=Зламана залежність
addon.broken_dependency.desc=Ця залежність існувала раніше, але тепер її немає. Спробуйте використовувати інше джерело завантаження.
addon.category=Категорія
addon.channel.alpha=Альфа
addon.channel.beta=Бета
Expand Down
2 changes: 0 additions & 2 deletions HMCL/src/main/resources/assets/lang/I18N_zh.properties
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,6 @@ account.skin.upload.failed=外觀上傳失敗
account.skin.invalid_skin=無法識別的外觀檔案
account.username=使用者名稱

addon.broken_dependency.title=損壞的相依內容
addon.broken_dependency.desc=該相依內容曾經存在於附加內容下載源中,但現在已被刪除,請嘗試其他下載源。
addon.category=類別
addon.changelog=更新日誌
addon.changelog.empty=暫無更新日誌
Expand Down
2 changes: 0 additions & 2 deletions HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,6 @@ account.skin.upload.failed=皮肤上传失败
account.skin.invalid_skin=无法识别的皮肤文件
account.username=用户名

addon.broken_dependency.title=损坏的前置内容
addon.broken_dependency.desc=该前置内容曾经在该附加内容下载源上存在过,但现在被删除了。换个下载源试试吧。
addon.category=类别
addon.changelog=更新日志
addon.changelog.empty=暂无更新日志
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Null

RemoteAddon getAddonById(DownloadProvider downloadProvider, String id) throws IOException;

default RemoteAddon resolveDependency(DownloadProvider downloadProvider, String id) throws IOException {
return getAddonById(downloadProvider, id);
}
/// @return the dependency resolved, or {@link RemoteAddon#BROKEN} when the dependency is not found
/// @throws IOException if an I/O error occurs, except for when the desired dependency is not found
RemoteAddon resolveDependency(DownloadProvider downloadProvider, String id) throws IOException;

RemoteAddon.File getAddonFile(String projectId, String fileId) throws IOException;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,13 @@
import org.jackhuang.hmcl.util.MurmurHash2;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.io.*;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
Expand Down Expand Up @@ -176,7 +175,7 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion
}

if (response == null) {
throw exception != null ? exception : new IOException("No candidates found");
throw exception != null ? exception : new NoCandidatesException();
}

// https://github.com/HMCL-dev/HMCL/issues/1549
Expand Down Expand Up @@ -254,6 +253,19 @@ public RemoteAddon getAddonById(DownloadProvider downloadProvider, String id) th
}
}

@Override
public RemoteAddon resolveDependency(DownloadProvider downloadProvider, String id) throws IOException {
try {
return getAddonById(downloadProvider, id);
} catch (IOException e) {
if (e instanceof NoCandidatesException) throw e;
if (e instanceof FileNotFoundException
|| e instanceof ResponseCodeException rce && rce.getResponseCode() == 404)
return RemoteAddon.BROKEN;
throw e;
}
}

@Override
public RemoteAddon.File getAddonFile(String projectId, String fileId) throws IOException {
SEMAPHORE.acquireUninterruptibly();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.io.NoCandidatesException;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -201,7 +202,7 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion
}
}

throw exception != null ? exception : new IOException("No candidates found");
throw exception != null ? exception : new NoCandidatesException();
} finally {
SEMAPHORE.release();
}
Expand Down Expand Up @@ -255,7 +256,7 @@ public RemoteAddon getAddonById(DownloadProvider downloadProvider, String id) th
}
}

throw exception != null ? exception : new IOException("No candidates found");
throw exception != null ? exception : new NoCandidatesException();
} finally {
SEMAPHORE.release();
}
Expand All @@ -265,13 +266,19 @@ public RemoteAddon getAddonById(DownloadProvider downloadProvider, String id) th
public RemoteAddon resolveDependency(DownloadProvider downloadProvider, String id) throws IOException {
try {
return getAddonById(downloadProvider, id);
} catch (ResponseCodeException e) {
if (e.getResponseCode() == 502 || e.getResponseCode() == 404) {
} catch (IOException e) {
if (e instanceof NoCandidatesException) throw e;
List<Throwable> l = new ArrayList<>(List.of(e));
l.addAll(List.of(e.getSuppressed()));
if (l.stream().allMatch(t -> {
var cause = t.getCause();
return cause == null
|| cause instanceof FileNotFoundException
|| cause instanceof ResponseCodeException rce && rce.getResponseCode() == 404;
})) { // Which means the file does not exist
return RemoteAddon.BROKEN;
Comment thread
ToobLac marked this conversation as resolved.
}
throw e;
} catch (FileNotFoundException e) {
return RemoteAddon.BROKEN;
}
}

Expand Down Expand Up @@ -307,7 +314,7 @@ public Stream<RemoteAddon.Version> getRemoteVersionsById(DownloadProvider downlo
}
}

throw exception != null ? exception : new IOException("No candidates found");
throw exception != null ? exception : new NoCandidatesException();
} finally {
SEMAPHORE.release();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.io;

import java.io.IOException;

public class NoCandidatesException extends IOException {

public NoCandidatesException() {
super("No candidates found");
}
}