Compare commits

...
26 Commits
Author SHA1 Message Date
Glavo 9969dc60c5 优化 ArchiveFileTree (#4177) 2025-08-02 19:33:44 +08:00
辞庐andGlavo 6230b436ad Fix #4158 : 缺少 Microsoft OAuth App ID 时禁用微软账户的刷新选项 (#4160)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-08-02 18:15:03 +08:00
Glavo 3873459aaa 通过 x-bmclapi-hash 缓存资源 (#4169) 2025-08-02 16:04:26 +08:00
3gf8jv4dv 4ee3857427 fix(locale): add missing modrinth category i18n keys (#4149) 2025-08-02 15:43:12 +08:00
Glavo 2617dd3630 修复无法加载 WebP 图标的问题 (#4171) 2025-08-02 15:37:48 +08:00
辞庐 e0425102c8 fix: 无法从 BMCLAPI 下载 NeoForge 所有所需文件 (#4174) 2025-08-02 15:33:42 +08:00
辞庐 a470475c21 模组下载列表翻页后自动回到顶部 (#4170) 2025-08-02 15:22:47 +08:00
Glavo bcd73e6d15 修复下载缓存不生效的问题 (#4167) 2025-08-01 20:53:32 +08:00
Glavo 7d6f21dc3a Bump Gradle to 9.0.0 (#4168) 2025-08-01 19:26:11 +08:00
GlavoandCopilot 3eddfa23b7 优化 JsonUtils (#4161)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-31 20:41:27 +08:00
Glavo d5fa232704 忽略 %ProgramFiles%\Common Files\Oracle\Java 中的 Java (#4142) 2025-07-31 20:17:27 +08:00
Glavo c84afb3155 通过主页右下角弹出菜单切换版本后隐藏菜单 (#4163) 2025-07-31 19:41:43 +08:00
Glavo 401fef5774 创建 checkstyle task (#4162) 2025-07-31 19:41:02 +08:00
Glavo 9298fe48e1 支持通过环境变量强制使用 GPU 渲染和控制动画帧率 (#4164) 2025-07-31 19:39:45 +08:00
Glavo 7f51e0395c 遵循系统动画开关设置 (#4148) 2025-07-31 15:37:40 +08:00
辞庐 4f410fb8ad 修复“下载线程数”的指示器被提示覆盖的问题 (#4153) 2025-07-31 15:05:23 +08:00
Glavo 9bcf95287a Fix #4146: 修复导入异常格式的数据包时启动器崩溃的问题 (#4152) 2025-07-31 15:04:56 +08:00
3gf8jv4dv 5703377626 fix(locale): restore accidentally removed translations (#4155) 2025-07-31 15:04:41 +08:00
Glavo 3adb3a67e9 将代码中的 URL 替换为 URI (#4131) 2025-07-30 18:19:46 +08:00
辞庐 7918b333e6 README 添加 CurseForge API Key 覆写参数 (#4147) 2025-07-30 15:34:29 +08:00
Glavo 329b0e8f3a 在 Windows 平台优先使用注册表探测 GPU (#4145) 2025-07-30 15:32:42 +08:00
辞庐 3b17e0a897 优化主页公告关闭按钮 (#4064) 2025-07-29 23:13:06 +08:00
辞庐 e10f1dac20 修复日志窗口内结束游戏进程会结束启动的所有游戏的问题 (#4130) 2025-07-29 17:37:42 +08:00
27Onion NebellandGlavo 86c171545e Update version check logic to support version like "26-ea" (#4128)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-07-29 17:28:24 +08:00
ShulkerSakuraandGlavo c3a3881cfc Added 16 characters length detection of username. (#4088)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-07-28 16:09:46 +08:00
Glavo 7f04cdd6b1 在日志中记录更多系统信息 (#4126) 2025-07-28 15:58:13 +08:00
140 changed files with 1900 additions and 1183 deletions

No files matched your search

+3 -3
View File
@@ -15,11 +15,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 11
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '11'
java-version: '17'
java-package: 'jdk+fx'
- name: Check Codes
run: ./gradlew checkstyleMain checkstyleTest checkTranslations --no-daemon --parallel
run: ./gradlew checkstyle checkTranslations --no-daemon --parallel
+2 -2
View File
@@ -13,11 +13,11 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up JDK 11
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '11'
java-version: '17'
java-package: 'jdk+fx'
- name: Build with Gradle
run: ./gradlew build --no-daemon
@@ -47,6 +47,21 @@ public final class EntryPoint {
createHMCLDirectories();
LOG.start(Metadata.HMCL_CURRENT_DIRECTORY.resolve("logs"));
if ("true".equalsIgnoreCase(System.getenv("HMCL_FORCE_GPU")))
System.getProperties().putIfAbsent("prism.forceGPU", "true");
String animationFrameRate = System.getenv("HMCL_ANIMATION_FRAME_RATE");
if (animationFrameRate != null) {
try {
if (Integer.parseInt(animationFrameRate) <= 0)
throw new NumberFormatException(animationFrameRate);
System.getProperties().putIfAbsent("javafx.animation.pulse", animationFrameRate);
} catch (NumberFormatException e) {
LOG.warning("Invalid animation frame rate: " + animationFrameRate);
}
}
checkDirectoryPath();
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ObservableBooleanValue;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ButtonType;
@@ -27,6 +28,7 @@ import javafx.scene.input.DataFormat;
import javafx.stage.Stage;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.setting.SambaException;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
import org.jackhuang.hmcl.task.Schedulers;
@@ -54,6 +56,8 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
@@ -78,6 +82,9 @@ public final class Launcher extends Application {
LOG.warning("Failed to get prism pipeline", e);
}
LOG.info("Dark Mode: " + Optional.ofNullable(FXUtils.DARK_MODE).map(ObservableBooleanValue::get).orElse(false));
LOG.info("Reduced Motion: " + Objects.requireNonNullElse(FXUtils.REDUCED_MOTION, false));
try {
try {
ConfigHolder.init();
@@ -158,6 +158,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
blackList.add("saves");
if (Files.exists(dstDir)) throw new IOException("Version exists");
Files.createDirectories(dstDir);
FileUtils.copyDirectory(srcDir, dstDir, path -> Modpack.acceptFile(path, blackList, null));
Path fromJson = srcDir.resolve(srcId + ".json");
@@ -170,7 +172,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
}
Files.copy(fromJson, toJson);
FileUtils.writeText(toJson, JsonUtils.GSON.toJson(fromVersion.setId(dstId)));
JsonUtils.writeToJsonFile(toJson, fromVersion.setId(dstId));
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
@@ -31,7 +31,6 @@ import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -66,7 +65,7 @@ public final class HMCLModpackInstallTask extends Task<Void> {
ModpackConfiguration<Modpack> config = null;
try {
if (json.exists()) {
config = JsonUtils.GSON.fromJson(Files.readString(json.toPath()), ModpackConfiguration.typeOf(Modpack.class));
config = JsonUtils.fromJsonFile(json.toPath(), ModpackConfiguration.typeOf(Modpack.class));
if (!HMCLModpackProvider.INSTANCE.getName().equals(config.getType()))
throw new IllegalArgumentException("Version " + name + " is not a HMCL modpack. Cannot update this version.");
@@ -53,7 +53,7 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URI;
import java.nio.file.AccessDeniedException;
import java.util.*;
import java.util.concurrent.*;
@@ -267,27 +267,27 @@ public final class LauncherHelper {
if (ex.getCause() instanceof ResponseCodeException) {
ResponseCodeException rce = (ResponseCodeException) ex.getCause();
int responseCode = rce.getResponseCode();
URL url = rce.getUrl();
URI uri = rce.getUri();
if (responseCode == 404)
message += i18n("download.code.404", url);
message += i18n("download.code.404", uri);
else
message += i18n("download.failed", url, responseCode);
message += i18n("download.failed", uri, responseCode);
} else {
message += StringUtils.getStackTrace(ex.getCause());
}
} else if (ex instanceof DownloadException) {
URL url = ((DownloadException) ex).getUrl();
URI uri = ((DownloadException) ex).getUri();
if (ex.getCause() instanceof SocketTimeoutException) {
message = i18n("install.failed.downloading.timeout", url);
message = i18n("install.failed.downloading.timeout", uri);
} else if (ex.getCause() instanceof ResponseCodeException) {
ResponseCodeException responseCodeException = (ResponseCodeException) ex.getCause();
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
message = i18n("download.code." + responseCodeException.getResponseCode(), url);
message = i18n("download.code." + responseCodeException.getResponseCode(), uri);
} else {
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
message = i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(ex.getCause());
}
} else {
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
message = i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(ex.getCause());
}
} else if (ex instanceof GameAssetIndexDownloadTask.GameAssetIndexMalformedException) {
message = i18n("assets.index.malformed");
@@ -298,11 +298,11 @@ public final class LauncherHelper {
} else if (ex instanceof ResponseCodeException) {
ResponseCodeException rce = (ResponseCodeException) ex;
int responseCode = rce.getResponseCode();
URL url = rce.getUrl();
URI uri = rce.getUri();
if (responseCode == 404)
message = i18n("download.code.404", url);
message = i18n("download.code.404", uri);
else
message = i18n("download.failed", url, responseCode);
message = i18n("download.failed", uri, responseCode);
} else if (ex instanceof CommandTooLongException) {
message = i18n("launch.failed.command_too_long");
} else if (ex instanceof ExecutionPolicyLimitException) {
@@ -140,7 +140,7 @@ public final class ModpackHelper {
throw new FileNotFoundException(file.getPath());
else
try {
return JsonUtils.GSON.fromJson(Files.readString(file.toPath()), ModpackConfiguration.class);
return JsonUtils.fromJsonFile(file.toPath(), ModpackConfiguration.class);
} catch (JsonParseException e) {
throw new IOException("Malformed modpack configuration");
}
@@ -45,7 +45,7 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
@@ -110,7 +110,7 @@ public final class TexturesLoader {
if (!Files.isRegularFile(file)) {
// download it
try {
new FileDownloadTask(new URL(texture.getUrl()), file.toFile()).run();
new FileDownloadTask(URI.create(texture.getUrl()), file).run();
LOG.info("Texture downloaded: " + texture.getUrl());
} catch (Exception e) {
if (Files.isRegularFile(file)) {
@@ -30,7 +30,6 @@ import org.jackhuang.hmcl.util.platform.Platform;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.util.*;
@@ -116,11 +115,7 @@ public final class HMCLJavaRepository implements JavaRepository {
}
if (Files.isDirectory(javaDir)) {
JavaManifest manifest;
try (InputStream input = Files.newInputStream(file)) {
manifest = JsonUtils.fromJsonFully(input, JavaManifest.class);
}
JavaManifest manifest = JsonUtils.fromJsonFile(file, JavaManifest.class);
list.add(JavaRuntime.of(executable, manifest.getInfo(), isManaged));
}
}
@@ -191,7 +186,7 @@ public final class HMCLJavaRepository implements JavaRepository {
});
JavaManifest manifest = new JavaManifest(info, update, files);
FileUtils.writeText(getManifestFile(platform, gameJavaVersion), JsonUtils.GSON.toJson(manifest));
JsonUtils.writeToJsonFile(getManifestFile(platform, gameJavaVersion), manifest);
return JavaRuntime.of(executable, info, true);
});
}
@@ -203,7 +198,7 @@ public final class HMCLJavaRepository implements JavaRepository {
throw new IOException("Platform is mismatch: expected " + platform + " but got " + result.getInfo().getPlatform());
Path executable = javaDir.resolve("bin").resolve(platform.getOperatingSystem().getJavaExecutable()).toRealPath();
FileUtils.writeText(getManifestFile(platform, name), JsonUtils.GSON.toJson(result));
JsonUtils.writeToJsonFile(getManifestFile(platform, name), result);
return JavaRuntime.of(executable, result.getInfo(), true);
});
}
@@ -411,8 +411,15 @@ public final class JavaManager {
if (System.getenv("PATH") != null) {
String[] paths = System.getenv("PATH").split(File.pathSeparator);
for (String path : paths) {
// https://github.com/HMCL-dev/HMCL/issues/4079
// https://github.com/Meloong-Git/PCL/issues/4261
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && path.toLowerCase(Locale.ROOT)
.contains("\\common files\\oracle\\java\\")) {
continue;
}
try {
tryAddJavaExecutable(javaRuntimes, Paths.get(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
tryAddJavaExecutable(javaRuntimes, Path.of(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
} catch (InvalidPathException ignored) {
}
}
@@ -72,8 +72,7 @@ public final class AuthlibInjectorServers implements Validation {
if (ConfigHolder.isNewlyCreated() && Files.exists(configLocation)) {
AuthlibInjectorServers configInstance;
try {
String content = Files.readString(configLocation);
configInstance = JsonUtils.GSON.fromJson(content, AuthlibInjectorServers.class);
configInstance = JsonUtils.fromJsonFile(configLocation, AuthlibInjectorServers.class);
} catch (IOException | JsonParseException e) {
LOG.warning("Malformed authlib-injectors.json", e);
return;
@@ -29,7 +29,7 @@ import org.jackhuang.hmcl.util.io.ResponseCodeException;
import javax.net.ssl.SSLHandshakeException;
import java.io.FileNotFoundException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URI;
import java.nio.file.AccessDeniedException;
import java.util.Arrays;
import java.util.Map;
@@ -137,26 +137,26 @@ public final class DownloadProviders {
public static String localizeErrorMessage(Throwable exception) {
if (exception instanceof DownloadException) {
URL url = ((DownloadException) exception).getUrl();
URI uri = ((DownloadException) exception).getUri();
if (exception.getCause() instanceof SocketTimeoutException) {
return i18n("install.failed.downloading.timeout", url);
return i18n("install.failed.downloading.timeout", uri);
} else if (exception.getCause() instanceof ResponseCodeException) {
ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause();
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
return i18n("download.code." + responseCodeException.getResponseCode(), url);
return i18n("download.code." + responseCodeException.getResponseCode(), uri);
} else {
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
return i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause());
}
} else if (exception.getCause() instanceof FileNotFoundException) {
return i18n("download.code.404", url);
return i18n("download.code.404", uri);
} else if (exception.getCause() instanceof AccessDeniedException) {
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
} else if (exception.getCause() instanceof ArtifactMalformedException) {
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.artifact_malformed");
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.artifact_malformed");
} else if (exception.getCause() instanceof SSLHandshakeException) {
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.ssl_handshake");
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.ssl_handshake");
} else {
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
return i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause());
}
} else if (exception instanceof ArtifactMalformedException) {
return i18n("exception.artifact_malformed");
@@ -18,13 +18,15 @@
package org.jackhuang.hmcl.setting;
import javafx.beans.InvalidationListener;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.net.*;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -32,7 +34,10 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class ProxyManager {
private static final ProxySelector NO_PROXY = new SimpleProxySelector(Proxy.NO_PROXY);
private static final ProxySelector SYSTEM_DEFAULT = Lang.requireNonNullElse(ProxySelector.getDefault(), NO_PROXY);
private static final ProxySelector SYSTEM_DEFAULT = Objects.requireNonNullElse(ProxySelector.getDefault(), NO_PROXY);
private static volatile @NotNull ProxySelector defaultProxySelector = SYSTEM_DEFAULT;
private static volatile @Nullable SimpleAuthenticator defaultAuthenticator = null;
private static ProxySelector getProxySelector() {
if (config().hasProxy()) {
@@ -53,7 +58,7 @@ public final class ProxyManager {
}
}
private static Authenticator getAuthenticator() {
private static SimpleAuthenticator getAuthenticator() {
if (config().hasProxy() && config().hasProxyAuth()) {
String username = config().getProxyUser();
String password = config().getProxyPass();
@@ -67,15 +72,34 @@ public final class ProxyManager {
}
static void init() {
ProxySelector.setDefault(getProxySelector());
InvalidationListener updateProxySelector = observable -> ProxySelector.setDefault(getProxySelector());
ProxySelector.setDefault(new ProxySelector() {
@Override
public List<Proxy> select(URI uri) {
return defaultProxySelector.select(uri);
}
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
defaultProxySelector.connectFailed(uri, sa, ioe);
}
});
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
var defaultAuthenticator = ProxyManager.defaultAuthenticator;
return defaultAuthenticator != null ? defaultAuthenticator.getPasswordAuthentication() : null;
}
});
defaultProxySelector = getProxySelector();
InvalidationListener updateProxySelector = observable -> defaultProxySelector = getProxySelector();
config().proxyTypeProperty().addListener(updateProxySelector);
config().proxyHostProperty().addListener(updateProxySelector);
config().proxyPortProperty().addListener(updateProxySelector);
config().hasProxyProperty().addListener(updateProxySelector);
Authenticator.setDefault(getAuthenticator());
InvalidationListener updateAuthenticator = observable -> Authenticator.setDefault(getAuthenticator());
defaultAuthenticator = getAuthenticator();
InvalidationListener updateAuthenticator = observable -> defaultAuthenticator = getAuthenticator();
config().hasProxyProperty().addListener(updateAuthenticator);
config().hasProxyAuthProperty().addListener(updateAuthenticator);
config().proxyUserProperty().addListener(updateAuthenticator);
@@ -124,7 +148,7 @@ public final class ProxyManager {
}
@Override
protected PasswordAuthentication getPasswordAuthentication() {
public PasswordAuthentication getPasswordAuthentication() {
return getRequestorType() == RequestorType.PROXY ? new PasswordAuthentication(username, password) : null;
}
}
@@ -25,9 +25,13 @@ import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.WeakListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.*;
import javafx.collections.ObservableMap;
import javafx.event.Event;
import javafx.event.EventDispatcher;
import javafx.event.EventType;
@@ -41,10 +45,7 @@ import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.*;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
@@ -54,9 +55,8 @@ import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.glavo.png.PNGType;
import org.glavo.png.PNGWriter;
import org.glavo.png.javafx.PNGJavaFXUtils;
import org.jackhuang.hmcl.task.CacheFileTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.util.*;
@@ -66,6 +66,7 @@ import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jetbrains.annotations.Nullable;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
@@ -78,11 +79,18 @@ import javax.imageio.stream.ImageInputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.awt.image.BufferedImage;
import java.io.*;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.ref.WeakReference;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@@ -105,6 +113,11 @@ public final class FXUtils {
public static final int JAVAFX_MAJOR_VERSION;
/// @see Platform.Preferences
public static final @Nullable ObservableMap<String, Object> PREFERENCES;
public static final @Nullable ObservableBooleanValue DARK_MODE;
public static final @Nullable Boolean REDUCED_MOTION;
static {
String jfxVersion = System.getProperty("javafx.version");
int majorVersion = -1;
@@ -115,6 +128,40 @@ public final class FXUtils {
}
}
JAVAFX_MAJOR_VERSION = majorVersion;
ObservableMap<String, Object> preferences = null;
ObservableBooleanValue darkMode = null;
Boolean reducedMotion = null;
if (JAVAFX_MAJOR_VERSION >= 22) {
try {
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
Class<?> preferencesClass = Class.forName("javafx.application.Platform$Preferences");
@SuppressWarnings("unchecked")
var preferences0 = (ObservableMap<String, Object>) lookup.findStatic(Platform.class, "getPreferences", MethodType.methodType(preferencesClass))
.invoke();
preferences = preferences0;
@SuppressWarnings("unchecked")
var colorSchemeProperty =
(ReadOnlyObjectProperty<? extends Enum<?>>)
lookup.findVirtual(preferencesClass, "colorSchemeProperty", MethodType.methodType(ReadOnlyObjectProperty.class))
.invoke(preferences);
darkMode = Bindings.createBooleanBinding(() ->
"DARK".equals(colorSchemeProperty.get().name()), colorSchemeProperty);
if (JAVAFX_MAJOR_VERSION >= 24) {
reducedMotion = (boolean)
lookup.findVirtual(preferencesClass, "isReducedMotion", MethodType.methodType(boolean.class))
.invoke(preferences);
}
} catch (Throwable e) {
LOG.warning("Failed to get preferences", e);
}
}
PREFERENCES = preferences;
DARK_MODE = darkMode;
REDUCED_MOTION = reducedMotion;
}
public static final String DEFAULT_MONOSPACE_FONT = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace";
@@ -124,18 +171,8 @@ public final class FXUtils {
);
private static final Map<String, Image> builtinImageCache = new ConcurrentHashMap<>();
private static final Map<String, Path> remoteImageCache = new ConcurrentHashMap<>();
public static void shutdown() {
for (Map.Entry<String, Path> entry : remoteImageCache.entrySet()) {
try {
Files.deleteIfExists(entry.getValue());
} catch (IOException e) {
LOG.warning(String.format("Failed to delete cache file %s.", entry.getValue()), e);
}
remoteImageCache.remove(entry.getKey());
}
builtinImageCache.clear();
}
@@ -799,14 +836,14 @@ public final class FXUtils {
}
}
public static Image loadImage(URL url) throws Exception {
URLConnection connection = NetworkUtils.createConnection(url);
public static Image loadImage(URI uri) throws Exception {
URLConnection connection = NetworkUtils.createConnection(uri);
if (connection instanceof HttpURLConnection) {
connection = NetworkUtils.resolveConnection((HttpURLConnection) connection);
}
try (InputStream input = connection.getInputStream()) {
String path = url.getPath();
String path = uri.getPath();
if (path != null && "webp".equalsIgnoreCase(StringUtils.substringAfterLast(path, '.')))
return loadWebPImage(input);
else {
@@ -858,78 +895,55 @@ public final class FXUtils {
}
}
/**
* Load image from the internet. It will cache the data of images for the further usage.
* The cached data will be deleted when HMCL is closed or hidden.
*
* @param url the url of image. The image resource should be a file on the internet.
* @return the image resource within the jar.
*/
public static Image newRemoteImage(String url) {
return newRemoteImage(url, 0, 0, false, false, false);
public static Task<Image> getRemoteImageTask(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth) {
return new CacheFileTask(URI.create(url))
.thenApplyAsync(file -> {
try (var channel = FileChannel.open(file, StandardOpenOption.READ)) {
var header = new byte[12];
var buffer = ByteBuffer.wrap(header);
//noinspection StatementWithEmptyBody
while (channel.read(buffer) > 0) {
}
channel.position(0L);
if (!buffer.hasRemaining()) {
// WebP File
if (header[0] == 'R' && header[1] == 'I' && header[2] == 'F' && header[3] == 'F' &&
header[8] == 'W' && header[9] == 'E' && header[10] == 'B' && header[11] == 'P') {
WebPImageReaderSpi spi = new WebPImageReaderSpi();
ImageReader reader = spi.createReaderInstance(null);
BufferedImage bufferedImage;
try (ImageInputStream imageInput = ImageIO.createImageInputStream(Channels.newInputStream(channel))) {
reader.setInput(imageInput, true, true);
bufferedImage = reader.read(0, reader.getDefaultReadParam());
} finally {
reader.dispose();
}
return SwingFXUtils.toFXImage(bufferedImage, requestedWidth, requestedHeight, preserveRatio, smooth);
}
}
Image image = new Image(Channels.newInputStream(channel), requestedWidth, requestedHeight, preserveRatio, smooth);
if (image.isError())
throw image.getException();
return image;
}
});
}
/**
* Load image from the internet. It will cache the data of images for the further usage.
* The cached data will be deleted when HMCL is closed or hidden.
*
* @param url the url of image. The image resource should be a file on the internet.
* @param requestedWidth the image's bounding box width
* @param requestedHeight the image's bounding box height
* @param preserveRatio indicates whether to preserve the aspect ratio of
* the original image when scaling to fit the image within the
* specified bounding box
* @param smooth indicates whether to use a better quality filtering
* algorithm or a faster one when scaling this image to fit within
* the specified bounding box
* @return the image resource within the jar.
*/
public static Image newRemoteImage(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth, boolean backgroundLoading) {
Path currentPath = remoteImageCache.get(url);
if (currentPath != null) {
if (Files.isReadable(currentPath)) {
try (InputStream inputStream = Files.newInputStream(currentPath)) {
return new Image(inputStream, requestedWidth, requestedHeight, preserveRatio, smooth);
} catch (IOException e) {
LOG.warning("An exception encountered while reading data from cached image file.", e);
}
}
// The file is unavailable or unreadable.
remoteImageCache.remove(url);
try {
Files.deleteIfExists(currentPath);
} catch (IOException e) {
LOG.warning("An exception encountered while deleting broken cached image file.", e);
}
}
Image image = new Image(url, requestedWidth, requestedHeight, preserveRatio, smooth, backgroundLoading);
image.progressProperty().addListener((observable, oldValue, newValue) -> {
if (newValue.doubleValue() >= 1.0 && !image.isError() && image.getPixelReader() != null && image.getWidth() > 0.0 && image.getHeight() > 0.0) {
Task.runAsync(() -> {
Path newPath = Files.createTempFile("hmcl-net-resource-cache-", ".cache");
try ( // Make sure the file is released from JVM before we put the path into remoteImageCache.
OutputStream outputStream = Files.newOutputStream(newPath);
PNGWriter writer = new PNGWriter(outputStream, PNGType.RGBA, PNGWriter.DEFAULT_COMPRESS_LEVEL)
) {
writer.write(PNGJavaFXUtils.asArgbImage(image));
} catch (IOException e) {
try {
Files.delete(newPath);
} catch (IOException e2) {
e2.addSuppressed(e);
throw e2;
}
throw e;
public static ObservableValue<Image> newRemoteImage(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth) {
var image = new SimpleObjectProperty<Image>();
getRemoteImageTask(url, requestedWidth, requestedHeight, preserveRatio, smooth)
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
image.set(result);
} else {
LOG.warning("An exception encountered while loading remote image: " + url, exception);
}
if (remoteImageCache.putIfAbsent(url, newPath) != null) {
Files.delete(newPath); // The image has been loaded in another task. Delete the image here in order not to pollute the tmp folder.
}
}).start();
}
});
})
.start();
return image;
}
@@ -190,10 +190,12 @@ public final class HTMLRenderer {
}
}
Image image = FXUtils.newRemoteImage(uri.toString(), width, height, true, true, false);
if (image.isError()) {
LOG.warning("Failed to load image: " + uri, image.getException());
} else {
try {
Image image = FXUtils.getRemoteImageTask(uri.toString(), width, height, true, true)
.run();
if (image == null)
throw new AssertionError("Image loading task returned null");
ImageView imageView = new ImageView(image);
if (hyperlink != null) {
URI target = resolveLink(hyperlink);
@@ -204,6 +206,8 @@ public final class HTMLRenderer {
}
children.add(imageView);
return;
} catch (Throwable e) {
LOG.warning("Failed to load image: " + uri, e);
}
}
@@ -38,7 +38,6 @@ import javafx.scene.input.MouseButton;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import org.jackhuang.hmcl.game.GameDumpGenerator;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.game.Log;
import org.jackhuang.hmcl.setting.StyleSheets;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
@@ -186,7 +185,7 @@ public final class LogWindow extends Stage {
}
private void onTerminateGame() {
LauncherHelper.stopManagedProcesses();
LogWindow.this.gameProcess.stop();
}
private void onClear() {
@@ -33,6 +33,7 @@ import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Theme;
@@ -128,6 +129,10 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
JFXButton btnRefresh = new JFXButton();
SpinnerPane spinnerRefresh = new SpinnerPane();
spinnerRefresh.getStyleClass().setAll("small-spinner-pane");
if (skinnable.getAccount() instanceof MicrosoftAccount && Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
btnRefresh.setDisable(true);
FXUtils.installFastTooltip(spinnerRefresh, i18n("account.methods.microsoft.snapshot"));
}
btnRefresh.setOnAction(e -> {
spinnerRefresh.showSpinner();
skinnable.refreshAsync()
@@ -31,10 +31,10 @@ import org.jackhuang.hmcl.ui.construct.DialogAware;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import javax.net.ssl.SSLException;
import java.io.IOException;
import java.net.URI;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
@@ -196,7 +196,7 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
lblServerName.setText(serverBeingAdded.getName());
lblServerUrl.setText(serverBeingAdded.getUrl());
lblServerWarning.setVisible("http".equals(NetworkUtils.toURL(serverBeingAdded.getUrl()).getProtocol()));
lblServerWarning.setVisible("http".equals(URI.create(serverBeingAdded.getUrl()).getScheme()));
this.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT);
} else {
@@ -267,7 +267,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
}).executor(true);
};
if (factory instanceof OfflineAccountFactory && username != null && !USERNAME_CHECKER_PATTERN.matcher(username).matches()) {
if (factory instanceof OfflineAccountFactory && username != null && (!USERNAME_CHECKER_PATTERN.matcher(username).matches() || username.length() > 16)) {
JFXButton btnYes = new JFXButton(i18n("button.ok"));
btnYes.getStyleClass().add("dialog-error");
btnYes.setOnAction(e -> doCreate.run());
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.ui.animation;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
/**
@@ -36,7 +37,7 @@ public final class AnimationUtils {
public static void init() {
}
private static final boolean ENABLED = !ConfigHolder.config().isAnimationDisabled();
private static final boolean ENABLED = !ConfigHolder.config().isAnimationDisabled() && FXUtils.REDUCED_MOTION != Boolean.TRUE;
private static final boolean PLAY_WINDOW_ANIMATION = ENABLED && !OperatingSystem.CURRENT_OS.isLinuxOrBSD();
public static boolean isAnimationEnabled() {
@@ -26,9 +26,7 @@ import javafx.scene.control.ScrollPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jetbrains.annotations.NotNull;
@@ -43,7 +41,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class TaskExecutorDialogPane extends BorderPane {
private TaskExecutor executor;
private TaskCancellationAction onCancel;
private final Consumer<FileDownloadTask.SpeedEvent> speedEventHandler;
private final Consumer<FetchTask.SpeedEvent> speedEventHandler;
private final Label lblTitle;
private final Label lblProgress;
@@ -108,7 +106,7 @@ public class TaskExecutorDialogPane extends BorderPane {
String finalUnit = unit;
Platform.runLater(() -> lblProgress.setText(String.format("%.1f %s", finalSpeed, finalUnit)));
};
FileDownloadTask.speedEvent.channel(FileDownloadTask.SpeedEvent.class).registerWeak(speedEventHandler);
FileDownloadTask.speedEvent.channel(FetchTask.SpeedEvent.class).registerWeak(speedEventHandler);
onEscPressed(this, btnCancel::fire);
}
@@ -58,7 +58,7 @@ import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.net.URL;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -214,7 +214,7 @@ public class DecoratorController {
String backgroundImageUrl = config().getBackgroundImageUrl();
if (backgroundImageUrl != null) {
try {
image = FXUtils.loadImage(new URL(backgroundImageUrl));
image = FXUtils.loadImage(URI.create(backgroundImageUrl));
} catch (Exception e) {
LOG.warning("Couldn't load background image", e);
}
@@ -52,10 +52,10 @@ import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
import java.net.URI;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Locale;
@@ -156,7 +156,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
Path dest = runDirectory.resolve(subdirectoryName).resolve(result);
Controllers.taskDialog(Task.composeAsync(() -> {
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(file.getFile().getUrl()), dest.toFile());
var task = new FileDownloadTask(URI.create(file.getFile().getUrl()), dest);
task.setName(file.getName());
return task;
}).whenComplete(Schedulers.javafx(), exception -> {
@@ -29,9 +29,7 @@ import javafx.scene.shape.SVGPath;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
@@ -43,7 +41,7 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
@@ -128,7 +126,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
private void onChooseRemoteFile() {
Controllers.prompt(i18n("modpack.choose.remote.tooltip"), (urlString, resolve, reject) -> {
try {
URL url = new URL(urlString);
URI url = URI.create(urlString);
if (urlString.endsWith("server-manifest.json")) {
// if urlString ends with .json, we assume that the url is server-manifest.json
Controllers.taskDialog(new GetTask(url).whenComplete(Schedulers.javafx(), (result, e) -> {
@@ -150,7 +148,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
resolve.run();
Controllers.taskDialog(
new FileDownloadTask(url, modpack.toFile(), null)
new FileDownloadTask(url, modpack, null)
.whenComplete(Schedulers.javafx(), e -> {
if (e == null) {
resolve.run();
@@ -37,7 +37,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -133,28 +133,28 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
if (exception.getCause() instanceof ResponseCodeException) {
ResponseCodeException rce = (ResponseCodeException) exception.getCause();
int responseCode = rce.getResponseCode();
URL url = rce.getUrl();
URI uri = rce.getUri();
if (responseCode == 404)
message += i18n("download.code.404", url);
message += i18n("download.code.404", uri);
else
message += i18n("download.failed", url, responseCode);
message += i18n("download.failed", uri, responseCode);
} else {
message += StringUtils.getStackTrace(exception.getCause());
}
Controllers.dialog(message, i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
} else if (exception instanceof DownloadException) {
URL url = ((DownloadException) exception).getUrl();
URI uri = ((DownloadException) exception).getUri();
if (exception.getCause() instanceof SocketTimeoutException) {
Controllers.dialog(i18n("install.failed.downloading.timeout", url), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
Controllers.dialog(i18n("install.failed.downloading.timeout", uri), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
} else if (exception.getCause() instanceof ResponseCodeException) {
ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause();
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
Controllers.dialog(i18n("download.code." + responseCodeException.getResponseCode(), url), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
Controllers.dialog(i18n("download.code." + responseCodeException.getResponseCode(), uri), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
} else {
Controllers.dialog(i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
Controllers.dialog(i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
}
} else {
Controllers.dialog(i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
Controllers.dialog(i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
}
} else if (exception instanceof UnsupportedInstallationException) {
switch (((UnsupportedInstallationException) exception).getReason()) {
@@ -49,6 +49,8 @@ import org.jackhuang.hmcl.download.quilt.QuiltAPIRemoteVersion;
import org.jackhuang.hmcl.download.quilt.QuiltRemoteVersion;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
@@ -67,7 +69,6 @@ import org.jackhuang.hmcl.util.i18n.I18n;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -96,7 +97,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
private final StackPane center;
private final VersionList<?> versionList;
private CompletableFuture<?> executor;
private Task<?> executor;
private final HBox searchBar;
private final StringProperty queryString = new SimpleStringProperty();
@@ -308,7 +309,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
public void refresh() {
VersionList<?> currentVersionList = versionList;
root.setContent(spinner, ContainerAnimations.FADE);
executor = currentVersionList.refreshAsync(gameVersion).whenComplete((result, exception) -> {
executor = currentVersionList.refreshAsync(gameVersion).whenComplete(Schedulers.defaultScheduler(), (result, exception) -> {
if (exception == null) {
List<RemoteVersion> items = loadVersions();
@@ -338,6 +339,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
// https://github.com/HMCL-dev/HMCL/issues/938
System.gc();
});
executor.start();
}
@Override
@@ -348,8 +350,9 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
@Override
public void cleanup(Map<String, Object> settings) {
settings.remove(libraryId);
if (executor != null)
executor.cancel(true);
// fixme
// if (executor != null)
// executor.cancel(true);
}
private void onRefresh() {
@@ -34,9 +34,6 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -117,8 +114,8 @@ public final class AboutPage extends StackPane {
return componentList;
}
try (Reader reader = new InputStreamReader(input, StandardCharsets.UTF_8)) {
JsonArray array = JsonUtils.GSON.fromJson(reader, JsonArray.class);
try {
JsonArray array = JsonUtils.fromJsonFully(input, JsonArray.class);
for (JsonElement element : array) {
JsonObject obj = element.getAsJsonObject();
@@ -124,6 +124,7 @@ public class DownloadSettingsPage extends StackPane {
{
HBox hbox = new HBox(8);
hbox.setStyle("-fx-view-order: -1;"); // prevent the indicator from being covered by the hint
hbox.setAlignment(Pos.CENTER);
hbox.setPadding(new Insets(0, 0, 0, 30));
hbox.disableProperty().bind(config().autoDownloadThreadsProperty());
@@ -363,7 +363,7 @@ public final class JavaDownloadDialog extends StackPane {
return getIntegrityCheck
.thenComposeAsync(integrityCheck ->
new FileDownloadTask(downloadProvider.injectURLWithCandidates(fileInfo.getDirectDownloadUri()),
targetFile, integrityCheck).setName(fileInfo.getFileName()))
targetFile.toPath(), integrityCheck).setName(fileInfo.getFileName()))
.thenSupplyAsync(targetFile::toPath);
})
.whenComplete(Schedulers.javafx(), ((result, exception) -> {
@@ -29,7 +29,6 @@ import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
@@ -125,18 +124,16 @@ public final class MainPage extends StackPane implements DecoratorPage {
titleBar.getStyleClass().add("title");
titleBar.setLeft(new Label(title));
Node hideNode = SVG.CLOSE.createIcon(Theme.blackFill(), 20);
hideNode.setCursor(Cursor.HAND);
titleBar.setRight(hideNode);
FXUtils.onClicked(hideNode, () -> {
if (announcementPane != null) {
if (Metadata.isDev()) {
config().getShownTips().put(ANNOUNCEMENT, Metadata.VERSION);
}
announcementPane.setContent(new StackPane(), ContainerAnimations.FADE);
JFXButton btnHide = new JFXButton();
btnHide.setOnAction(e -> {
announcementPane.setContent(new StackPane(), ContainerAnimations.FADE);
if (Metadata.isDev()) {
config().getShownTips().put(ANNOUNCEMENT, Metadata.VERSION);
}
});
btnHide.getStyleClass().add("announcement-close-button");
btnHide.setGraphic(SVG.CLOSE.createIcon(Theme.blackFill(), 20));
titleBar.setRight(btnHide);
TextFlow body = FXUtils.segmentToTextFlow(content, Controllers::onHyperlinkAction);
body.setLineSpacing(4);
@@ -279,7 +276,10 @@ public final class MainPage extends StackPane implements DecoratorPage {
FXUtils.onClicked(menu, popup::hide);
versionNodes = MappedObservableList.create(versions, version -> {
Node node = PopupMenu.wrapPopupMenuItem(new GameItem(profile, version.getId()));
FXUtils.onClicked(node, () -> profile.setSelectedVersion(version.getId()));
FXUtils.onClicked(node, () -> {
profile.setSelectedVersion(version.getId());
popup.hide();
});
return node;
});
Bindings.bindContent(menu.getContent(), versionNodes);
@@ -68,6 +68,7 @@ import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class DownloadListPage extends Control implements DecoratorPage, VersionPage.VersionLoadable {
protected final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
@@ -229,6 +230,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
}
private static class ModDownloadListPageSkin extends SkinBase<DownloadListPage> {
private final JFXListView<RemoteMod> listView = new JFXListView<>();
protected ModDownloadListPageSkin(DownloadListPage control) {
super(control);
@@ -449,6 +451,8 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
boolean disableNext = disableAll || pageOffset == pageCount - 1;
nextPageButton.setDisable(disableNext);
lastPageButton.setDisable(disableNext);
listView.scrollTo(0);
};
FXUtils.onChange(control.pageCount, pageCountN -> {
@@ -504,7 +508,6 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
}
});
JFXListView<RemoteMod> listView = new JFXListView<>();
spinnerPane.setContent(listView);
Bindings.bindContent(listView.getItems(), getSkinnable().items);
FXUtils.onClicked(listView, () -> {
@@ -540,7 +543,8 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
.collect(Collectors.toList()));
if (StringUtils.isNotBlank(dataItem.getIconUrl())) {
imageView.setImage(FXUtils.newRemoteImage(dataItem.getIconUrl(), 40, 40, true, true, true));
LOG.debug("Icon: " + dataItem.getIconUrl());
imageView.imageProperty().bind(FXUtils.newRemoteImage(dataItem.getIconUrl(), 40, 40, true, true));
}
}
});
@@ -49,12 +49,12 @@ import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.net.URI;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -180,7 +180,7 @@ public class DownloadPage extends Control implements DecoratorPage {
Controllers.taskDialog(
Task.composeAsync(() -> {
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(file.getFile().getUrl()), dest, file.getFile().getIntegrityCheck());
var task = new FileDownloadTask(URI.create(file.getFile().getUrl()), dest.toPath(), file.getFile().getIntegrityCheck());
task.setName(file.getName());
return task;
}),
@@ -220,7 +220,7 @@ public class DownloadPage extends Control implements DecoratorPage {
{
ImageView imageView = new ImageView();
if (StringUtils.isNotBlank(getSkinnable().addon.getIconUrl())) {
imageView.setImage(FXUtils.newRemoteImage(getSkinnable().addon.getIconUrl(), 40, 40, true, true, true));
imageView.imageProperty().bind(FXUtils.newRemoteImage(getSkinnable().addon.getIconUrl(), 40, 40, true, true));
}
descriptionPane.getChildren().add(FXUtils.limitingSize(imageView, 40, 40));
@@ -359,7 +359,7 @@ public class DownloadPage extends Control implements DecoratorPage {
.collect(Collectors.toList()));
if (StringUtils.isNotBlank(addon.getIconUrl())) {
imageView.setImage(FXUtils.newRemoteImage(addon.getIconUrl(), 40, 40, true, true, true));
imageView.imageProperty().bind(FXUtils.newRemoteImage(addon.getIconUrl(), 40, 40, true, true));
}
} else {
content.setTitle(i18n("mods.broken_dependency.title"));
@@ -45,7 +45,7 @@ import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.CSVTable;
import java.net.URL;
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
@@ -293,9 +293,9 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
if (isDisabled)
fileName += ModManager.DISABLED_EXTENSION;
FileDownloadTask task = new FileDownloadTask(
new URL(remote.getFile().getUrl()),
modManager.getModsDirectory().resolve(fileName).toFile());
var task = new FileDownloadTask(
URI.create(remote.getFile().getUrl()),
modManager.getModsDirectory().resolve(fileName));
task.setName(remote.getName());
return task;
@@ -28,10 +28,7 @@ import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.account.CreateAccountPane;
@@ -47,7 +44,8 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CancellationException;
@@ -75,18 +73,18 @@ public final class Versions {
public static void downloadModpackImpl(Profile profile, String version, RemoteMod.Version file) {
Path modpack;
URL downloadURL;
URI downloadURL;
try {
modpack = Files.createTempFile("modpack", ".zip");
downloadURL = new URL(file.getFile().getUrl());
} catch (IOException e) {
downloadURL = new URI(file.getFile().getUrl());
} catch (IOException | URISyntaxException e) {
Controllers.dialog(
i18n("install.failed.downloading.detail", file.getFile().getUrl()) + "\n" + StringUtils.getStackTrace(e),
i18n("download.failed.no_code"), MessageDialogPane.MessageType.ERROR);
return;
}
Controllers.taskDialog(
new FileDownloadTask(downloadURL, modpack.toFile())
new FileDownloadTask(downloadURL, modpack)
.whenComplete(Schedulers.javafx(), e -> {
if (e == null) {
if (version != null) {
@@ -19,21 +19,21 @@ package org.jackhuang.hmcl.upgrade;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.Pack200Utils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.tukaani.xz.XZInputStream;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.jar.JarOutputStream;
class HMCLDownloadTask extends FileDownloadTask {
final class HMCLDownloadTask extends FileDownloadTask {
private RemoteVersion.Type archiveFormat;
private final RemoteVersion.Type archiveFormat;
public HMCLDownloadTask(RemoteVersion version, Path target) {
super(NetworkUtils.toURL(version.getUrl()), target.toFile(), version.getIntegrityCheck());
super(URI.create(version.getUrl()), target, version.getIntegrityCheck());
archiveFormat = version.getType();
}
@@ -42,8 +42,7 @@ class HMCLDownloadTask extends FileDownloadTask {
super.execute();
try {
Path target = getFile().toPath();
Path target = getPath();
switch (archiveFormat) {
case JAR:
break;
@@ -51,7 +50,7 @@ class HMCLDownloadTask extends FileDownloadTask {
case PACK_XZ:
byte[] raw = Files.readAllBytes(target);
try (InputStream in = new XZInputStream(new ByteArrayInputStream(raw));
JarOutputStream out = new JarOutputStream(Files.newOutputStream(target))) {
JarOutputStream out = new JarOutputStream(Files.newOutputStream(target))) {
Pack200Utils.unpack(in, out);
}
break;
@@ -60,7 +59,11 @@ class HMCLDownloadTask extends FileDownloadTask {
throw new IllegalArgumentException("Unknown format: " + archiveFormat);
}
} catch (Throwable e) {
getFile().delete();
try {
Files.deleteIfExists(getPath());
} catch (Throwable e2) {
e.addSuppressed(e2);
}
throw e;
}
}
@@ -26,13 +26,14 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.net.URI;
import java.util.Optional;
public class RemoteVersion {
public static RemoteVersion fetch(UpdateChannel channel, String url) throws IOException {
try {
JsonObject response = JsonUtils.fromNonNullJson(NetworkUtils.doGet(NetworkUtils.toURL(url)), JsonObject.class);
JsonObject response = JsonUtils.fromNonNullJson(NetworkUtils.doGet(URI.create(url)), JsonObject.class);
String version = Optional.ofNullable(response.get("version")).map(JsonElement::getAsString).orElseThrow(() -> new IOException("version is missing"));
String jarUrl = Optional.ofNullable(response.get("jar")).map(JsonElement::getAsString).orElse(null);
String jarHash = Optional.ofNullable(response.get("jarsha1")).map(JsonElement::getAsString).orElse(null);
@@ -28,6 +28,7 @@ import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.net.URI;
import java.util.HashMap;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -122,7 +123,7 @@ public final class CrashReporter implements Thread.UncaughtExceptionHandler {
map.put("version", Metadata.VERSION);
map.put("log", LOG.getLogs());
try {
String response = NetworkUtils.doPost(NetworkUtils.toURL(Metadata.PUBLISH_URL + "/hmcl/crash.php"), map);
String response = NetworkUtils.doPost(URI.create(Metadata.PUBLISH_URL + "/hmcl/crash.php"), map);
if (StringUtils.isNotBlank(response))
LOG.error("Crash server response: " + response);
} catch (IOException ex) {
@@ -120,5 +120,43 @@ public final class SwingFXUtils {
pw.setPixels(0, 0, bw, bh, pf, data, offset, scan);
return wimg;
}
public static WritableImage toFXImage(BufferedImage bimg, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth) {
if (requestedWidth <= 0. || requestedHeight <= 0.) {
return toFXImage(bimg, null);
}
int width = (int) requestedWidth;
int height = (int) requestedHeight;
// Calculate actual dimensions if preserveRatio is true
if (preserveRatio) {
double originalWidth = bimg.getWidth();
double originalHeight = bimg.getHeight();
double scaleX = requestedWidth / originalWidth;
double scaleY = requestedHeight / originalHeight;
double scale = Math.min(scaleX, scaleY);
width = (int) (originalWidth * scale);
height = (int) (originalHeight * scale);
}
// Create scaled BufferedImage
BufferedImage scaledImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE);
Graphics2D g2d = scaledImage.createGraphics();
try {
if (smooth) {
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
}
g2d.drawImage(bimg, 0, 0, width, height, null);
} finally {
g2d.dispose();
}
return toFXImage(scaledImage, null);
}
}
@@ -1080,6 +1080,23 @@
-jfx-mask-type: CIRCLE;
}
.announcement-close-button {
-fx-announcement-close-button-size: 20px;
-fx-pref-width: -fx-announcement-close-button-size;
-fx-max-width: -fx-announcement-close-button-size;
-fx-min-width: -fx-announcement-close-button-size;
-fx-pref-height: -fx-announcement-close-button-size;
-fx-max-height: -fx-announcement-close-button-size;
-fx-min-height: -fx-announcement-close-button-size;
-fx-background-radius: 50px;
-fx-background-color: transparent;
}
.announcement-close-button .jfx-rippler {
-jfx-rippler-fill: -fx-base-check-color;
-jfx-mask-type: CIRCLE;
}
.transparent {
-fx-background-color: null;
}
@@ -124,7 +124,7 @@ account.methods.microsoft.purchase=Buy Minecraft
account.methods.microsoft.snapshot=You are using an unofficial build of HMCL. Please download the official build to log in.
account.methods.microsoft.snapshot.website=Official Website
account.methods.offline=Offline
account.methods.offline.name.special_characters=Using only English letters, numbers, and underscores is recommended
account.methods.offline.name.special_characters=Use only letters, numbers, and underscores (max 16 chars)
account.methods.offline.name.invalid=It is recommended to use only English letters, numbers and underscores for the username, and the length should not exceed 16 characters.\n\
\n\
\ · Legitimate: HuangYu, huang_Yu, Huang_Yu_123;\n\
@@ -930,17 +930,20 @@ modpack.wizard.step.initialization.server=Click here for more information on how
modrinth.category.adventure=Adventure
modrinth.category.atmosphere=Atmosphere
modrinth.category.audio=Audio
modrinth.category.babric=Babric
modrinth.category.blocks=Blocks
modrinth.category.bloom=Bloom
modrinth.category.bta-babric=BTA (Babric)
modrinth.category.bukkit=Bukkit
modrinth.category.bungeecord=BungeeCord
modrinth.category.canvas=Canvas
modrinth.category.cartoon=Cartoon
modrinth.category.challenging=Challenging
modrinth.category.colored-lighting=Colored Lighting
modrinth.category.core-shaders=Core Shaders
modrinth.category.combat=Combat
modrinth.category.core-shaders=Core Shaders
modrinth.category.cursed=Cursed
modrinth.category.datapack=Datapack
modrinth.category.decoration=Decoration
modrinth.category.economy=Economy
modrinth.category.entities=Entities
@@ -948,6 +951,7 @@ modrinth.category.environment=Environment
modrinth.category.equipment=Equipment
modrinth.category.fabric=Fabric
modrinth.category.fantasy=Fantasy
modrinth.category.folia=Folia
modrinth.category.foliage=Foliage
modrinth.category.fonts=Fonts
modrinth.category.food=Food
@@ -957,7 +961,9 @@ modrinth.category.gui=GUI
modrinth.category.high=High
modrinth.category.iris=Iris
modrinth.category.items=Items
modrinth.category.java-agent=Java Agent
modrinth.category.kitchen-sink=Kitchen-Sink
modrinth.category.legacy-fabric=Legacy Fabric
modrinth.category.library=Library
modrinth.category.lightweight=Lightweight
modrinth.category.liteloader=LiteLoader
@@ -975,8 +981,10 @@ modrinth.category.models=Models
modrinth.category.modloader=Modloader
modrinth.category.multiplayer=Multiplayer
modrinth.category.neoforge=NeoForge
modrinth.category.nilloader=NilLoader
modrinth.category.optifine=OptiFine
modrinth.category.optimization=Optimization
modrinth.category.ornithe=Ornithe
modrinth.category.paper=Paper
modrinth.category.path-tracing=Path Tracing
modrinth.category.pbr=PBR
@@ -1005,8 +1013,6 @@ modrinth.category.vanilla-like=Vanilla-like
modrinth.category.velocity=Velocity
modrinth.category.waterfall=Waterfall
modrinth.category.worldgen=Worldgen
modrinth.category.datapack=Datapack
modrinth.category.folia=Folia
modrinth.category.8x-=8x-
modrinth.category.16x=16x
modrinth.category.32x=32x
@@ -435,6 +435,7 @@ folder.shaderpacks=Paquetes de sombreados
folder.saves=Mundos
folder.schematics=Esquemas
folder.screenshots=Capturas de pantalla
folder.world=Directorio del mundo
game=Juegos
game.crash.feedback=<b>¡Por favor, no comparta capturas de pantalla de esta interfaz con otras personas!</b> Si pide ayuda a otras personas, haga clic en <b>Exportar registros de colgado</b> y envíe el archivo exportado a otras personas para que lo analicen.
@@ -932,33 +933,50 @@ modpack.wizard.step.initialization.warning=Antes de hacer un modpack, por favor
modpack.wizard.step.initialization.server=Haga clic aquí para ver más tutoriales para hacer un modpack de servidor que se pueda actualizar automáticamente.
modrinth.category.adventure=Aventura
modrinth.category.atmosphere=Atmósfera
modrinth.category.audio=Audio
modrinth.category.babric=Babric
modrinth.category.blocks=Bloqueos
modrinth.category.bloom=Resplandor
modrinth.category.bta-babric=BTA (Babric)
modrinth.category.bukkit=Bukkit
modrinth.category.bungeecord=BungeeCord
modrinth.category.canvas=Canvas
modrinth.category.cartoon=Caricatura
modrinth.category.challenging=Desafío
modrinth.category.core-shaders=Sombreadores de núcleo
modrinth.category.colored-lighting=Iluminación de color
modrinth.category.combat=Combate
modrinth.category.core-shaders=Sombreadores de núcleo
modrinth.category.cursed=Maldición
modrinth.category.datapack=Paquete de datos
modrinth.category.decoration=Decoración
modrinth.category.economy=Economía
modrinth.category.entities=Entidades
modrinth.category.environment=Medio ambiente
modrinth.category.equipment=Equipo
modrinth.category.fabric=Fabric
modrinth.category.fantasy=Fantasía
modrinth.category.folia=Folia
modrinth.category.foliage=Vegetación
modrinth.category.fonts=Fonts
modrinth.category.food=Alimentos
modrinth.category.forge=Forge
modrinth.category.game-mechanics=Mecánica de juego
modrinth.category.gui=GUI
modrinth.category.high=Alto
modrinth.category.iris=Iris
modrinth.category.items=Objetos
modrinth.category.java-agent=Java Agent
modrinth.category.kitchen-sink=Fregadero de cocina
modrinth.category.legacy-fabric=Legacy Fabric
modrinth.category.library=Biblioteca
modrinth.category.lightweight=Peso ligero
modrinth.category.liteloader=LiteLoader
modrinth.category.locale=Locale
modrinth.category.low=Bajo
modrinth.category.magic=Magia
modrinth.category.management=Gestión
modrinth.category.medium=Medio
modrinth.category.minecraft=Minecraft
modrinth.category.minigame=Minijuego
modrinth.category.misc=Misceláneo
@@ -968,13 +986,23 @@ modrinth.category.models=Modelos
modrinth.category.modloader=Cargador de mods
modrinth.category.multiplayer=Multijugador
modrinth.category.neoforge=NeoForge
modrinth.category.nilloader=NilLoader
modrinth.category.optifine=OptiFine
modrinth.category.optimization=Optimización
modrinth.category.ornithe=Ornithe
modrinth.category.paper=Paper
modrinth.category.path-tracing=Trazado de rutas
modrinth.category.pbr=PBR
modrinth.category.potato=Mínimo
modrinth.category.purpur=Purpur
modrinth.category.quests=Misiones
modrinth.category.quilt=Quilt
modrinth.category.realistic=Realista
modrinth.category.reflections=Reflexiones
modrinth.category.rift=Rift
modrinth.category.screenshot=Extremo
modrinth.category.semi-realistic=Semirrealista
modrinth.category.shadows=Sombras
modrinth.category.simplistic=Simplista
modrinth.category.social=Social
modrinth.category.spigot=Spigot
@@ -985,12 +1013,11 @@ modrinth.category.themed=Temática
modrinth.category.transportation=Transporte
modrinth.category.tweaks=Ajustes
modrinth.category.utility=Utilidad
modrinth.category.vanilla=Vainilla
modrinth.category.vanilla-like=Similar a la vainilla
modrinth.category.velocity=Velocity
modrinth.category.waterfall=Waterfall
modrinth.category.worldgen=Generación de mundos
modrinth.category.datapack=Paquete de datos
modrinth.category.folia=Folia
modrinth.category.8x-=8x-
modrinth.category.16x=16x
modrinth.category.32x=32x
@@ -1072,7 +1099,12 @@ world.backup.create.locked=El mundo está actualmente en uso. Por favor, cierra
world.backup.create.success=Creada con éxito una nueva copia de seguridad: %s
world.backup.delete=Eliminar esta copia de seguridad
world.backup.processing=Creando nueva copia de seguridad ...
world.datapack=Gestionar paquetes de datos
world.chunkbase=Chunk Base
world.chunkbase.end_city=Ciudad del End
world.chunkbase.seed_map=Vista previa de la generación mundial
world.chunkbase.stronghold=Fortaleza
world.chunkbase.nether_fortress=Fortaleza del Nether
world.datapack=Paquetes de datos
world.datetime=Jugado por última vez en %s
world.download=Descargar Mundo
world.download.title=Descargar mundo - %1s
@@ -1117,7 +1149,8 @@ world.info.time=Tiempo de juego
world.info.time.format=%s días
world.locked=En uso
world.manage=Mundos
world.manage.title=Mundos - %s
world.manage.button=Administrar
world.manage.title=Mundo - %s
world.name=Nombre del mundo
world.name.enter=Introducir el nombre del mundo
world.show_all=Mostrar todo
@@ -433,6 +433,7 @@ folder.shaderpacks=Пакеты шейдеров
folder.saves=Миры
folder.schematics=Схемы
folder.screenshots=Снимки экрана
folder.world=Папка мира
game=Игры
game.crash.feedback=<b>Пожалуйста, не делитесь скриншотами этого интерфейса с другими!</b> Если вы просите помощи у других, нажмите <b>«Экспорт журналов с ошибками»</b> и отправьте экспортированный файл другим для анализа.
@@ -931,33 +932,50 @@ modpack.wizard.step.initialization.warning=Перед созданием мод
modpack.wizard.step.initialization.server=Нажмите здесь для получения дополнительных руководств по созданию модпака на сервере, который может автообновляться.
modrinth.category.adventure=Приключение
modrinth.category.atmosphere=Атмосфера
modrinth.category.audio=Аудио
modrinth.category.babric=Babric
modrinth.category.blocks=Блоки
modrinth.category.bloom=Свечение
modrinth.category.bta-babric=BTA (Babric)
modrinth.category.bukkit=Bukkit
modrinth.category.bungeecord=BungeeCord
modrinth.category.canvas=Canvas
modrinth.category.cartoon=Мультяшный
modrinth.category.challenging=Вызов
modrinth.category.core-shaders=Ядро шейдеров
modrinth.category.colored-lighting=Цветное освещение
modrinth.category.combat=Бой
modrinth.category.core-shaders=Ядро шейдеров
modrinth.category.cursed=Cursed
modrinth.category.datapack=Набор данных
modrinth.category.decoration=Украшения
modrinth.category.economy=Экономика
modrinth.category.entities=Сущности
modrinth.category.environment=Окружающая среда
modrinth.category.equipment=Оборудование
modrinth.category.fabric=Fabric
modrinth.category.fantasy=Фэнтези
modrinth.category.folia=Folia
modrinth.category.foliage=Растительность
modrinth.category.fonts=Шрифты
modrinth.category.food=Еда
modrinth.category.forge=Forge
modrinth.category.game-mechanics=Игровая механика
modrinth.category.gui=ГИП
modrinth.category.high=Высокий
modrinth.category.iris=Iris
modrinth.category.items=Предметы
modrinth.category.java-agent=Java Agent
modrinth.category.kitchen-sink=Kitchen-Sink
modrinth.category.legacy-fabric=Legacy Fabric
modrinth.category.library=Библиотека
modrinth.category.lightweight=Легкий
modrinth.category.liteloader=LiteLoader
modrinth.category.locale=Язык
modrinth.category.low=Низкий
modrinth.category.magic=Магия
modrinth.category.management=Управление
modrinth.category.medium=Средний
modrinth.category.minecraft=Minecraft
modrinth.category.minigame=Мини-игра
modrinth.category.misc=Разное
@@ -967,13 +985,23 @@ modrinth.category.models=Модели
modrinth.category.modloader=Мод-загрузчик
modrinth.category.multiplayer=Сетевая игра
modrinth.category.neoforge=NeoForge
modrinth.category.nilloader=NilLoader
modrinth.category.optifine=OptiFine
modrinth.category.optimization=Оптимизация
modrinth.category.ornithe=Ornithe
modrinth.category.paper=Paper
modrinth.category.path-tracing=Трассировка пути
modrinth.category.pbr=PBR
modrinth.category.potato=Минимум
modrinth.category.purpur=Purpur
modrinth.category.quests=Квесты
modrinth.category.quilt=Quilt
modrinth.category.realistic=Реалистичный
modrinth.category.reflections=Отражения
modrinth.category.rift=Rift
modrinth.category.screenshot=Экстрим
modrinth.category.semi-realistic=Полуреалистичный
modrinth.category.shadows=Тени
modrinth.category.simplistic=Упрощенный
modrinth.category.social=Социальная
modrinth.category.spigot=Spigot
@@ -984,12 +1012,11 @@ modrinth.category.themed=Тематический
modrinth.category.transportation=Транспорт
modrinth.category.tweaks=Усовершенствование
modrinth.category.utility=Утилиты
modrinth.category.vanilla=Ванильное
modrinth.category.vanilla-like=Ванильное
modrinth.category.velocity=Velocity
modrinth.category.waterfall=Waterfall
modrinth.category.worldgen=Генерация мира
modrinth.category.datapack=Набор данных
modrinth.category.folia=Folia
modrinth.category.8x-=8x-
modrinth.category.16x=16x
modrinth.category.32x=32x
@@ -1071,7 +1098,12 @@ world.backup.create.locked=В настоящее время мир находи
world.backup.create.success=Успешно создано новое резервное копирование: %s
world.backup.delete=Удалить эту резервную копию
world.backup.processing=Создание новой резервной копии ...
world.datapack=Управлять наборами данных
world.chunkbase=Chunk Base
world.chunkbase.end_city=Город Края
world.chunkbase.seed_map=Предпросмотр генерации мира
world.chunkbase.stronghold=Крепость
world.chunkbase.nether_fortress=Крепость Нижнего мира
world.datapack=Наборы данных
world.datetime=Последний запуск игры %s
world.download=Скачать мир
world.download.title=Скачать мир - %1s
@@ -1116,7 +1148,8 @@ world.info.time=Время игры
world.info.time.format=%s дн.
world.locked=В эксплуатации
world.manage=Миры
world.manage.title=Миры - %s
world.manage.button=Управлять
world.manage.title=Мир - %s
world.name=Название мира
world.name.enter=Введите название мира
world.show_all=Показать все
@@ -131,7 +131,7 @@ account.methods.ban_query=查詢帳戶是否被封禁
account.methods.microsoft.snapshot=你正在使用第三方提供的 HMCL。請下載官方版本進行登入。
account.methods.microsoft.snapshot.website=官方網站
account.methods.offline=離線模式
account.methods.offline.name.special_characters=建議使用英文字母、數字以及底線命名
account.methods.offline.name.special_characters=建議使用英文字母、數字以及底線命名,且長度不超過 16 個字元
account.methods.offline.name.invalid=遊戲使用者名稱建議僅使用英文字母、數字及底線,且長度不超過 16 個字元。\n\
\n\
\ · 一些有效的使用者名稱:HuangYu、huang_Yu、Huang_Yu_123;\n\
@@ -744,17 +744,20 @@ modpack.wizard.step.initialization.server=點選此處查看有關伺服器自
modrinth.category.adventure=冒險
modrinth.category.atmosphere=氛圍
modrinth.category.audio=聲音
modrinth.category.babric=Babric
modrinth.category.blocks=方塊
modrinth.category.bloom=泛光
modrinth.category.bta-babric=BTA (Babric)
modrinth.category.bukkit=Bukkit
modrinth.category.bungeecord=BungeeCord
modrinth.category.canvas=Canvas
modrinth.category.cartoon=卡通
modrinth.category.challenging=高難度
modrinth.category.colored-lighting=彩色光照
modrinth.category.core-shaders=核心著色器
modrinth.category.combat=戰鬥
modrinth.category.core-shaders=核心著色器
modrinth.category.cursed=Cursed
modrinth.category.datapack=資料包
modrinth.category.decoration=裝飾
modrinth.category.economy=經濟
modrinth.category.entities=實體
@@ -762,6 +765,7 @@ modrinth.category.environment=環境
modrinth.category.equipment=裝備
modrinth.category.fabric=Fabric
modrinth.category.fantasy=幻想
modrinth.category.folia=Folia
modrinth.category.foliage=植被
modrinth.category.fonts=字體
modrinth.category.food=食物
@@ -771,7 +775,9 @@ modrinth.category.gui=GUI
modrinth.category.high=高
modrinth.category.iris=Iris
modrinth.category.items=物品
modrinth.category.java-agent=Java Agent
modrinth.category.kitchen-sink=大雜燴
modrinth.category.legacy-fabric=Legacy Fabric
modrinth.category.library=支援庫
modrinth.category.lightweight=輕量
modrinth.category.liteloader=LiteLoader
@@ -789,8 +795,10 @@ modrinth.category.models=模型
modrinth.category.modloader=ModLoader
modrinth.category.multiplayer=多人
modrinth.category.neoforge=NeoForge
modrinth.category.nilloader=NilLoader
modrinth.category.optifine=OptiFine
modrinth.category.optimization=最佳化
modrinth.category.ornithe=Ornithe
modrinth.category.paper=Paper
modrinth.category.path-tracing=路径追踪
modrinth.category.pbr=PBR
@@ -819,8 +827,6 @@ modrinth.category.vanilla-like=類原生
modrinth.category.velocity=Velocity
modrinth.category.waterfall=Waterfall
modrinth.category.worldgen=世界生成
modrinth.category.datapack=資料包
modrinth.category.folia=Folia
mods=模組
mods.add=新增模組
@@ -139,7 +139,7 @@ account.methods.ban_query=检测账户是否被封禁
account.methods.microsoft.snapshot=你正在使用第三方提供的 HMCL。请下载官方版本来登录微软账户。
account.methods.microsoft.snapshot.website=官方网站
account.methods.offline=离线模式
account.methods.offline.name.special_characters=建议使用英文字符、数字以及下划线命名
account.methods.offline.name.special_characters=建议使用英文字符、数字以及下划线命名,且长度不超过 16 个字符
account.methods.offline.name.invalid=游戏用户名建议仅使用英文字母、数字及下划线,且长度不超过 16 个字符。\n\
\n\
\ · 一些合法用户名:HuangYu、huang_Yu、Huang_Yu_123;\n\
@@ -754,17 +754,20 @@ modpack.wizard.step.initialization.server=点击此处查看有关服务器自
modrinth.category.adventure=冒险
modrinth.category.atmosphere=氛围
modrinth.category.audio=声音
modrinth.category.babric=Babric
modrinth.category.blocks=方块
modrinth.category.bloom=泛光
modrinth.category.bta-babric=BTA (Babric)
modrinth.category.bukkit=Bukkit
modrinth.category.bungeecord=BungeeCord
modrinth.category.canvas=Canvas
modrinth.category.cartoon=卡通
modrinth.category.challenging=高难度
modrinth.category.colored-lighting=彩色光照
modrinth.category.core-shaders=核心着色器
modrinth.category.combat=战斗
modrinth.category.core-shaders=核心着色器
modrinth.category.cursed=Cursed
modrinth.category.datapack=数据包
modrinth.category.decoration=装饰
modrinth.category.economy=经济
modrinth.category.entities=实体
@@ -772,6 +775,7 @@ modrinth.category.environment=环境
modrinth.category.equipment=装备
modrinth.category.fabric=Fabric
modrinth.category.fantasy=幻想
modrinth.category.folia=Folia
modrinth.category.foliage=植被
modrinth.category.fonts=字体
modrinth.category.food=食物
@@ -781,7 +785,9 @@ modrinth.category.gui=GUI
modrinth.category.high=高
modrinth.category.iris=Iris
modrinth.category.items=物品
modrinth.category.java-agent=Java Agent
modrinth.category.kitchen-sink=大杂烩
modrinth.category.legacy-fabric=Legacy Fabric
modrinth.category.library=支持库
modrinth.category.lightweight=轻量
modrinth.category.liteloader=LiteLoader
@@ -799,8 +805,10 @@ modrinth.category.models=模型
modrinth.category.modloader=Modloader
modrinth.category.multiplayer=多人
modrinth.category.neoforge=NeoForge
modrinth.category.nilloader=NilLoader
modrinth.category.optifine=OptiFine
modrinth.category.optimization=优化
modrinth.category.ornithe=Ornithe
modrinth.category.paper=Paper
modrinth.category.path-tracing=路径追踪
modrinth.category.pbr=PBR
@@ -829,8 +837,6 @@ modrinth.category.vanilla-like=类原生
modrinth.category.velocity=Velocity
modrinth.category.waterfall=Waterfall
modrinth.category.worldgen=世界生成
modrinth.category.datapack=数据包
modrinth.category.folia=Folia
mods=模组
mods.add=添加模组
@@ -28,40 +28,53 @@ public final class Main {
private Main() {
}
/**
* Check if the current Java version is compatible with HMCL.
*/
static boolean checkJavaVersion(String javaVersion) {
if (javaVersion == null) {
return false;
static int findFirstNotNumber(String str, int start) {
if (start >= str.length())
return -1;
char ch = str.charAt(start);
if (ch < '0' || ch > '9')
return -1;
for (int i = start + 1; i < str.length(); i++) {
ch = str.charAt(i);
if (ch < '0' || ch > '9')
return i;
}
return str.length();
}
static int getJavaFeatureVersion(String javaVersion) {
if (javaVersion == null)
return -1;
try {
int major;
int dot = javaVersion.indexOf('.');
int end = findFirstNotNumber(javaVersion, 0);
if (end < 0)
return -1; // No valid version number found
if (dot >= 0) {
major = Integer.parseInt(javaVersion.substring(0, dot));
if (major == 1 && dot < javaVersion.length() - 1) {
int begin = dot + 1;
dot = javaVersion.indexOf('.', begin);
int major = Integer.parseInt(javaVersion.substring(0, end));
if (major > 1)
return major;
major = dot > begin
? Integer.parseInt(javaVersion.substring(begin, dot))
: Integer.parseInt(javaVersion.substring(begin));
}
} else {
major = Integer.parseInt(javaVersion);
}
if (major < 1)
return -1; // Invalid major version
return major >= MINIMUM_JAVA_VERSION;
// Java 1.x versions
int start = end + 1;
end = findFirstNotNumber(javaVersion, start);
if (end < 0)
return -1; // No valid minor version found
return Integer.parseInt(javaVersion.substring(start, end));
} catch (NumberFormatException e) {
return false;
return -1; // version number is too long
}
}
public static void main(String[] args) throws Throwable {
if (checkJavaVersion(System.getProperty("java.version"))) {
if (getJavaFeatureVersion(System.getProperty("java.version")) >= MINIMUM_JAVA_VERSION) {
EntryPoint.main(args);
} else {
String errorMessage = BootProperties.getResourceBundle().getString("boot.unsupported_java_version");
@@ -19,21 +19,30 @@ package org.jackhuang.hmcl;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.*;
public final class MainTest {
@Test
public void testCheckJavaVersion() {
assertFalse(Main.checkJavaVersion("1.6.0"));
assertFalse(Main.checkJavaVersion("1.6.0_45"));
assertFalse(Main.checkJavaVersion("1.7.0"));
assertFalse(Main.checkJavaVersion("1.7.0_80"));
assertFalse(Main.checkJavaVersion("1.8"));
assertFalse(Main.checkJavaVersion("1.8.0_321"));
public void testGetJavaFeatureVersion() {
assertEquals(6, Main.getJavaFeatureVersion("1.6.0"));
assertEquals(6, Main.getJavaFeatureVersion("1.6.0_45"));
assertEquals(7, Main.getJavaFeatureVersion("1.7.0"));
assertEquals(7, Main.getJavaFeatureVersion("1.7.0_80"));
assertEquals(8, Main.getJavaFeatureVersion("1.8"));
assertEquals(8, Main.getJavaFeatureVersion("1.8u321"));
assertEquals(8, Main.getJavaFeatureVersion("1.8.0_321"));
assertEquals(11, Main.getJavaFeatureVersion("11"));
assertEquals(11, Main.getJavaFeatureVersion("11.0.26"));
assertEquals(21, Main.getJavaFeatureVersion("21"));
assertEquals(26, Main.getJavaFeatureVersion("26-ea"));
assertTrue(Main.checkJavaVersion("11"));
assertTrue(Main.checkJavaVersion("11.0.26"));
assertTrue(Main.checkJavaVersion("21"));
assertEquals(-1, Main.getJavaFeatureVersion(null));
assertEquals(-1, Main.getJavaFeatureVersion(""));
assertEquals(-1, Main.getJavaFeatureVersion("0"));
assertEquals(-1, Main.getJavaFeatureVersion("0.8"));
assertEquals(-1, Main.getJavaFeatureVersion("abc"));
assertEquals(-1, Main.getJavaFeatureVersion("1.abc"));
assertEquals(-1, Main.getJavaFeatureVersion(".1"));
assertEquals(-1, Main.getJavaFeatureVersion("1111111111111111111111"));
}
}
@@ -25,7 +25,7 @@ import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.util.io.HttpRequest;
import java.io.IOException;
import java.net.URL;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
@@ -96,7 +96,7 @@ public class AuthlibInjectorDownloader implements AuthlibInjectorArtifactProvide
}
try {
new FileDownloadTask(downloadProvider.get().injectURLWithCandidates(latest.downloadUrl), artifactLocation.toFile(),
new FileDownloadTask(downloadProvider.get().injectURLWithCandidates(latest.downloadUrl), artifactLocation,
Optional.ofNullable(latest.checksums.get("sha256"))
.map(checksum -> new IntegrityCheck("SHA-256", checksum))
.orElse(null))
@@ -110,9 +110,9 @@ public class AuthlibInjectorDownloader implements AuthlibInjectorArtifactProvide
private AuthlibInjectorVersionInfo getLatestArtifactInfo() throws IOException {
IOException exception = null;
for (URL url : downloadProvider.get().injectURLWithCandidates(LATEST_BUILD_URL)) {
for (URI url : downloadProvider.get().injectURLWithCandidates(LATEST_BUILD_URL)) {
try {
return HttpRequest.GET(url.toExternalForm()).getJson(AuthlibInjectorVersionInfo.class);
return HttpRequest.GET(url.toString()).getJson(AuthlibInjectorVersionInfo.class);
} catch (IOException | JsonParseException e) {
if (exception == null) {
exception = new IOException("Failed to fetch authlib-injector artifact info");
@@ -17,13 +17,11 @@
*/
package org.jackhuang.hmcl.auth.authlibinjector;
import static org.jackhuang.hmcl.util.io.NetworkUtils.toURL;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilProvider;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import java.net.URL;
import java.net.URI;
import java.util.UUID;
public class AuthlibInjectorProvider implements YggdrasilProvider {
@@ -35,33 +33,33 @@ public class AuthlibInjectorProvider implements YggdrasilProvider {
}
@Override
public URL getAuthenticationURL() throws AuthenticationException {
return toURL(apiRoot + "authserver/authenticate");
public URI getAuthenticationURL() throws AuthenticationException {
return URI.create(apiRoot + "authserver/authenticate");
}
@Override
public URL getRefreshmentURL() throws AuthenticationException {
return toURL(apiRoot + "authserver/refresh");
public URI getRefreshmentURL() throws AuthenticationException {
return URI.create(apiRoot + "authserver/refresh");
}
@Override
public URL getValidationURL() throws AuthenticationException {
return toURL(apiRoot + "authserver/validate");
public URI getValidationURL() throws AuthenticationException {
return URI.create(apiRoot + "authserver/validate");
}
@Override
public URL getInvalidationURL() throws AuthenticationException {
return toURL(apiRoot + "authserver/invalidate");
public URI getInvalidationURL() throws AuthenticationException {
return URI.create(apiRoot + "authserver/invalidate");
}
@Override
public URL getSkinUploadURL(UUID uuid) throws UnsupportedOperationException {
return toURL(apiRoot + "api/user/profile/" + UUIDTypeAdapter.fromUUID(uuid) + "/skin");
public URI getSkinUploadURL(UUID uuid) throws UnsupportedOperationException {
return URI.create(apiRoot + "api/user/profile/" + UUIDTypeAdapter.fromUUID(uuid) + "/skin");
}
@Override
public URL getProfilePropertiesURL(UUID uuid) throws AuthenticationException {
return toURL(apiRoot + "sessionserver/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid));
public URI getProfilePropertiesURL(UUID uuid) throws AuthenticationException {
return URI.create(apiRoot + "sessionserver/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid));
}
@Override
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.auth.authlibinjector;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.emptyMap;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -25,14 +24,15 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import java.io.IOException;
import java.lang.reflect.Type;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import org.jetbrains.annotations.Nullable;
@@ -58,17 +58,14 @@ public class AuthlibInjectorServer implements Observable {
public static AuthlibInjectorServer locateServer(String url) throws IOException {
try {
url = addHttpsIfMissing(url);
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestProperty("Accept-Language", Locale.getDefault().toLanguageTag());
HttpURLConnection conn = NetworkUtils.createHttpConnection(URI.create(url));
String ali = conn.getHeaderField("x-authlib-injector-api-location");
if (ali != null) {
URL absoluteAli = new URL(conn.getURL(), ali);
URI absoluteAli = conn.getURL().toURI().resolve(ali);
if (!urlEqualsIgnoreSlash(url, absoluteAli.toString())) {
conn.disconnect();
url = absoluteAli.toString();
conn = (HttpURLConnection) absoluteAli.openConnection();
conn.setRequestProperty("Accept-Language", Locale.getDefault().toLanguageTag());
conn = NetworkUtils.createHttpConnection(absoluteAli);
}
}
@@ -77,22 +74,26 @@ public class AuthlibInjectorServer implements Observable {
try {
AuthlibInjectorServer server = new AuthlibInjectorServer(url);
server.refreshMetadata(new String(conn.getInputStream().readAllBytes(), UTF_8));
server.refreshMetadata(NetworkUtils.readFullyAsString(conn));
return server;
} finally {
conn.disconnect();
}
} catch (IllegalArgumentException e) {
} catch (IllegalArgumentException | URISyntaxException e) {
throw new IOException(e);
}
}
private static String addHttpsIfMissing(String url) {
String lowercased = url.toLowerCase(Locale.ROOT);
if (!lowercased.startsWith("http://") && !lowercased.startsWith("https://")) {
url = "https://" + url;
private static String addHttpsIfMissing(String url) throws IOException {
URI uri = URI.create(url);
if (uri.getScheme() == null) {
return "https://" + url;
} else if (!NetworkUtils.isHttpUri(uri)) {
throw new IOException("Yggdrasil server should be an HTTP or HTTPS URI, but got: " + url);
} else {
return url;
}
return url;
}
private static boolean urlEqualsIgnoreSlash(String a, String b) {
@@ -103,7 +104,7 @@ public class AuthlibInjectorServer implements Observable {
return a.equals(b);
}
private String url;
private final String url;
@Nullable
private String metadataResponse;
private long metadataTimestamp;
@@ -34,7 +34,8 @@ import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
@@ -164,7 +165,7 @@ public class MicrosoftService {
.accept("application/json").createConnection();
if (request.getResponseCode() != 200) {
throw new ResponseCodeException(new URL("https://api.minecraftservices.com/entitlements/mcstore"), request.getResponseCode());
throw new ResponseCodeException(URI.create("https://api.minecraftservices.com/entitlements/mcstore"), request.getResponseCode());
}
// Get Minecraft Account UUID
@@ -247,22 +248,22 @@ public class MicrosoftService {
if (responseCode == HTTP_NOT_FOUND) {
throw new NoMinecraftJavaEditionProfileException();
} else if (responseCode != 200) {
throw new ResponseCodeException(new URL("https://api.minecraftservices.com/minecraft/profile"), responseCode);
throw new ResponseCodeException(URI.create("https://api.minecraftservices.com/minecraft/profile"), responseCode);
}
String result = NetworkUtils.readData(conn);
String result = NetworkUtils.readFullyAsString(conn);
return JsonUtils.fromNonNullJson(result, MinecraftProfileResponse.class);
}
public Optional<CompleteGameProfile> getCompleteGameProfile(UUID uuid) throws AuthenticationException {
Objects.requireNonNull(uuid);
return Optional.ofNullable(GSON.fromJson(request(NetworkUtils.toURL("https://sessionserver.mojang.com/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid)), null), CompleteGameProfile.class));
return Optional.ofNullable(GSON.fromJson(request(URI.create("https://sessionserver.mojang.com/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid)), null), CompleteGameProfile.class));
}
public void uploadSkin(String accessToken, boolean isSlim, Path file) throws AuthenticationException, UnsupportedOperationException {
try {
HttpURLConnection con = NetworkUtils.createHttpConnection(NetworkUtils.toURL("https://api.minecraftservices.com/minecraft/profile/skins"));
HttpURLConnection con = NetworkUtils.createHttpConnection(URI.create("https://api.minecraftservices.com/minecraft/profile/skins"));
con.setRequestMethod("POST");
con.setRequestProperty("Authorization", "Bearer " + accessToken);
con.setDoOutput(true);
@@ -273,21 +274,21 @@ public class MicrosoftService {
}
}
String response = NetworkUtils.readData(con);
String response = NetworkUtils.readFullyAsString(con);
if (StringUtils.isBlank(response)) {
if (con.getResponseCode() / 100 != 2)
throw new ResponseCodeException(con.getURL(), con.getResponseCode());
throw new ResponseCodeException(con.getURL().toURI(), con.getResponseCode());
} else {
MinecraftErrorResponse profileResponse = GSON.fromJson(response, MinecraftErrorResponse.class);
if (StringUtils.isNotBlank(profileResponse.errorMessage) || con.getResponseCode() / 100 != 2)
throw new AuthenticationException("Failed to upload skin, response code: " + con.getResponseCode() + ", response: " + response);
}
} catch (IOException | JsonParseException e) {
} catch (IOException | JsonParseException | URISyntaxException e) {
throw new AuthenticationException(e);
}
}
private static String request(URL url, Object payload) throws AuthenticationException {
private static String request(URI url, Object payload) throws AuthenticationException {
try {
if (payload == null)
return NetworkUtils.doGet(url);
@@ -33,14 +33,11 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URI;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.*;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.tryCast;
@@ -169,7 +166,7 @@ public class Skin {
String realCslApi = type == Type.LITTLE_SKIN
? "https://littleskin.cn/csl"
: StringUtils.removeSuffix(Lang.requireNonNullElse(cslApi, ""), "/");
return Task.composeAsync(() -> new GetTask(new URL(String.format("%s/%s.json", realCslApi, username))))
return Task.composeAsync(() -> new GetTask(URI.create(String.format("%s/%s.json", realCslApi, username))))
.thenComposeAsync(json -> {
SkinJson result = JsonUtils.GSON.fromJson(json, SkinJson.class);
@@ -179,8 +176,8 @@ public class Skin {
return Task.allOf(
Task.supplyAsync(result::getModel),
result.getHash() == null ? Task.supplyAsync(() -> null) : new FetchBytesTask(new URL(String.format("%s/textures/%s", realCslApi, result.getHash())), 3),
result.getCapeHash() == null ? Task.supplyAsync(() -> null) : new FetchBytesTask(new URL(String.format("%s/textures/%s", realCslApi, result.getCapeHash())), 3)
result.getHash() == null ? Task.supplyAsync(() -> null) : new FetchBytesTask(URI.create(String.format("%s/textures/%s", realCslApi, result.getHash())), 3),
result.getCapeHash() == null ? Task.supplyAsync(() -> null) : new FetchBytesTask(URI.create(String.format("%s/textures/%s", realCslApi, result.getCapeHash())), 3)
);
}).thenApplyAsync(result -> {
if (result == null) {
@@ -232,8 +229,8 @@ public class Skin {
private static class FetchBytesTask extends FetchTask<InputStream> {
public FetchBytesTask(URL url, int retry) {
super(Collections.singletonList(url), retry);
public FetchBytesTask(URI uri, int retry) {
super(List.of(uri), retry);
}
@Override
@@ -247,7 +244,7 @@ public class Skin {
}
@Override
protected Context getContext(URLConnection conn, boolean checkETag) throws IOException {
protected Context getContext(URLConnection connection, boolean checkETag, String bmclapiHash) throws IOException {
return new Context() {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -263,7 +260,7 @@ public class Skin {
setResult(new ByteArrayInputStream(baos.toByteArray()));
if (checkETag) {
repository.cacheBytes(baos.toByteArray(), conn);
repository.cacheBytes(connection, baos.toByteArray());
}
}
};
@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.auth.yggdrasil;
import org.jackhuang.hmcl.auth.AuthenticationException;
import java.net.URL;
import java.net.URI;
import java.util.UUID;
/**
@@ -27,13 +27,13 @@ import java.util.UUID;
*/
public interface YggdrasilProvider {
URL getAuthenticationURL() throws AuthenticationException;
URI getAuthenticationURL() throws AuthenticationException;
URL getRefreshmentURL() throws AuthenticationException;
URI getRefreshmentURL() throws AuthenticationException;
URL getValidationURL() throws AuthenticationException;
URI getValidationURL() throws AuthenticationException;
URL getInvalidationURL() throws AuthenticationException;
URI getInvalidationURL() throws AuthenticationException;
/**
* URL to upload skin.
@@ -51,8 +51,8 @@ public interface YggdrasilProvider {
* @throws AuthenticationException if url cannot be generated. e.g. some parameter or query is malformed.
* @throws UnsupportedOperationException if the Yggdrasil provider does not support third-party skin uploading.
*/
URL getSkinUploadURL(UUID uuid) throws AuthenticationException, UnsupportedOperationException;
URI getSkinUploadURL(UUID uuid) throws AuthenticationException, UnsupportedOperationException;
URL getProfilePropertiesURL(UUID uuid) throws AuthenticationException;
URI getProfilePropertiesURL(UUID uuid) throws AuthenticationException;
}
@@ -34,7 +34,7 @@ import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
@@ -160,7 +160,7 @@ public class YggdrasilService {
request.file("file", FileUtils.getName(file), "image/" + FileUtils.getExtension(file), fis);
}
}
requireEmpty(NetworkUtils.readData(con));
requireEmpty(NetworkUtils.readFullyAsString(con));
} catch (IOException e) {
throw new AuthenticationException(e);
}
@@ -227,12 +227,12 @@ public class YggdrasilService {
}
}
private static String request(URL url, Object payload) throws AuthenticationException {
private static String request(URI uri, Object payload) throws AuthenticationException {
try {
if (payload == null)
return NetworkUtils.doGet(url);
return NetworkUtils.doGet(uri);
else
return NetworkUtils.doPost(url, payload instanceof String ? (String) payload : GSON.toJson(payload), "application/json");
return NetworkUtils.doPost(uri, payload instanceof String ? (String) payload : GSON.toJson(payload), "application/json");
} catch (IOException e) {
throw new ServerDisconnectException(e);
}
@@ -17,7 +17,7 @@
*/
package org.jackhuang.hmcl.download;
import java.net.URL;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@@ -59,21 +59,21 @@ public class AdaptedDownloadProvider implements DownloadProvider {
}
@Override
public List<URL> getAssetObjectCandidates(String assetObjectLocation) {
public List<URI> getAssetObjectCandidates(String assetObjectLocation) {
return downloadProviderCandidates.stream()
.flatMap(d -> d.getAssetObjectCandidates(assetObjectLocation).stream())
.collect(Collectors.toList());
}
@Override
public List<URL> injectURLWithCandidates(String baseURL) {
public List<URI> injectURLWithCandidates(String baseURL) {
return downloadProviderCandidates.stream()
.flatMap(d -> d.injectURLWithCandidates(baseURL).stream())
.collect(Collectors.toList());
}
@Override
public List<URL> injectURLsWithCandidates(List<String> urls) {
public List<URI> injectURLsWithCandidates(List<String> urls) {
return downloadProviderCandidates.stream()
.flatMap(d -> d.injectURLsWithCandidates(urls).stream())
.collect(Collectors.toList());
@@ -17,7 +17,7 @@
*/
package org.jackhuang.hmcl.download;
import java.net.URL;
import java.net.URI;
import java.util.List;
/**
@@ -51,17 +51,17 @@ public class AutoDownloadProvider implements DownloadProvider {
}
@Override
public List<URL> getAssetObjectCandidates(String assetObjectLocation) {
public List<URI> getAssetObjectCandidates(String assetObjectLocation) {
return fileProvider.getAssetObjectCandidates(assetObjectLocation);
}
@Override
public List<URL> injectURLWithCandidates(String baseURL) {
public List<URI> injectURLWithCandidates(String baseURL) {
return fileProvider.injectURLWithCandidates(baseURL);
}
@Override
public List<URL> injectURLsWithCandidates(List<String> urls) {
public List<URI> injectURLsWithCandidates(List<String> urls) {
return fileProvider.injectURLsWithCandidates(urls);
}
@@ -71,8 +71,7 @@ public final class BMCLAPIDownloadProvider implements DownloadProvider {
pair("http://files.minecraftforge.net/maven", apiRoot + "/maven"),
pair("https://files.minecraftforge.net/maven", apiRoot + "/maven"),
pair("https://maven.minecraftforge.net", apiRoot + "/maven"),
pair("https://maven.neoforged.net/releases/net/neoforged/forge", apiRoot + "/maven/net/neoforged/forge"),
pair("https://maven.neoforged.net/releases/net/neoforged/neoforge", apiRoot + "/maven/net/neoforged/neoforge"),
pair("https://maven.neoforged.net/releases/", apiRoot + "/maven/"),
pair("http://dl.liteloader.com/versions/versions.json", apiRoot + "/maven/com/mumfrey/liteloader/versions.json"),
pair("http://dl.liteloader.com/versions", apiRoot + "/maven"),
pair("https://meta.fabricmc.net", apiRoot + "/fabric-meta"),
@@ -29,13 +29,14 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.NotNull;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -43,7 +44,6 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class DefaultCacheRepository extends CacheRepository {
private Path librariesDir;
private Path indexFile;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private Index index = null;
public DefaultCacheRepository() {
@@ -64,10 +64,13 @@ public class DefaultCacheRepository extends CacheRepository {
lock.writeLock().lock();
try {
if (Files.isRegularFile(indexFile)) {
index = JsonUtils.fromNonNullJson(Files.readString(indexFile), Index.class);
}
else
index = JsonUtils.fromJsonFile(indexFile, Index.class);
if (index == null) {
throw new JsonParseException("Index file is empty or invalid");
}
} else {
index = new Index();
}
} catch (IOException | JsonParseException e) {
LOG.warning("Unable to read index file", e);
index = new Index();
@@ -82,7 +85,7 @@ public class DefaultCacheRepository extends CacheRepository {
* If cannot be verified, the library will not be cached.
*
* @param library the library being cached
* @param jar the file of library
* @param jar the file of library
*/
public void tryCacheLibrary(Library library, Path jar) {
lock.readLock().lock();
@@ -171,8 +174,8 @@ public class DefaultCacheRepository extends CacheRepository {
* Caches the library file to repository.
*
* @param library the library to cache
* @param path the file being cached, must be verified
* @param forge true if this library is provided by Forge
* @param path the file being cached, must be verified
* @param forge true if this library is provided by Forge
* @return cached file location
* @throws IOException if failed to calculate hash code of {@code path} or copy the file to cache
*/
@@ -200,25 +203,30 @@ public class DefaultCacheRepository extends CacheRepository {
private void saveIndex() {
if (indexFile == null || index == null) return;
try {
FileUtils.writeText(indexFile, JsonUtils.GSON.toJson(index));
Files.createDirectories(indexFile.getParent());
FileUtils.saveSafely(indexFile, outputStream -> {
try (var writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8))) {
JsonUtils.GSON.toJson(index, writer);
}
});
} catch (IOException e) {
LOG.error("Unable to save index.json", e);
}
}
/**
* {
* "libraries": {
* // allow a library has multiple hash code.
* [
* "name": "net.minecraftforge:forge:1.11.2-13.20.0.2345",
* "hash": "blablabla",
* "type": "forge"
* ]
* }
* // assets and versions will not be included in index.
* }
*/
/// ```json
/// {
/// "libraries": {
/// // allow a library has multiple hash code.
/// [
/// "name": "net.minecraftforge:forge:1.11.2-13.20.0.2345",
/// "hash": "blablabla",
/// "type": "forge"
/// ]
/// }
/// }
///```
/// assets and versions will not be included in index.
private static final class Index implements Validation {
private final Set<LibraryIndex> libraries;
@@ -148,7 +148,7 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
if (baseVersion.isResolved()) throw new IllegalArgumentException("Version should not be resolved");
VersionList<?> versionList = getVersionList(libraryId);
return Task.fromCompletableFuture(versionList.loadAsync(gameVersion))
return versionList.loadAsync(gameVersion)
.thenComposeAsync(() -> installLibraryAsync(baseVersion, versionList.getVersion(gameVersion, libraryVersion)
.orElseThrow(() -> new IOException("Remote library " + libraryId + " has no version " + libraryVersion))))
.withStage(String.format("hmcl.install.%s:%s", libraryId, libraryVersion));
@@ -17,10 +17,7 @@
*/
package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.net.URL;
import java.util.Collections;
import java.net.URI;
import java.util.List;
import java.util.stream.Collectors;
@@ -35,8 +32,8 @@ public interface DownloadProvider {
String getAssetBaseURL();
default List<URL> getAssetObjectCandidates(String assetObjectLocation) {
return Collections.singletonList(NetworkUtils.toURL(getAssetBaseURL() + assetObjectLocation));
default List<URI> getAssetObjectCandidates(String assetObjectLocation) {
return List.of(URI.create(getAssetBaseURL() + assetObjectLocation));
}
/**
@@ -59,11 +56,11 @@ public interface DownloadProvider {
* @param baseURL original URL provided by Mojang and Forge.
* @return the URL that is equivalent to [baseURL], but belongs to your own service provider.
*/
default List<URL> injectURLWithCandidates(String baseURL) {
return Collections.singletonList(NetworkUtils.toURL(injectURL(baseURL)));
default List<URI> injectURLWithCandidates(String baseURL) {
return List.of(URI.create(injectURL(baseURL)));
}
default List<URL> injectURLsWithCandidates(List<String> urls) {
default List<URI> injectURLsWithCandidates(List<String> urls) {
return urls.stream().flatMap(url -> injectURLWithCandidates(url).stream()).collect(Collectors.toList());
}
@@ -17,8 +17,9 @@
*/
package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.task.Task;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -40,44 +41,40 @@ public class MultipleSourceVersionList extends VersionList<RemoteVersion> {
}
@Override
public CompletableFuture<?> loadAsync() {
public Task<?> loadAsync() {
throw new UnsupportedOperationException("MultipleSourceVersionList does not support loading the entire remote version list.");
}
@Override
public CompletableFuture<?> refreshAsync() {
public Task<?> refreshAsync() {
throw new UnsupportedOperationException("MultipleSourceVersionList does not support loading the entire remote version list.");
}
private CompletableFuture<?> refreshAsync(String gameVersion, int sourceIndex) {
private Task<?> refreshAsync(String gameVersion, int sourceIndex) {
VersionList<?> versionList = backends[sourceIndex];
CompletableFuture<Void> future = versionList.refreshAsync(gameVersion)
.thenRunAsync(() -> {
return versionList.refreshAsync(gameVersion)
.thenComposeAsync(() -> {
lock.writeLock().lock();
try {
versions.putAll(gameVersion, versionList.getVersions(gameVersion));
} catch (Exception e) {
if (sourceIndex == backends.length - 1) {
LOG.warning("Failed to fetch versions list from all sources", e);
throw e;
} else {
LOG.warning("Failed to fetch versions list and try to fetch from other source", e);
return refreshAsync(gameVersion, sourceIndex + 1);
}
} finally {
lock.writeLock().unlock();
}
return null;
});
if (sourceIndex == backends.length - 1) {
return future;
} else {
return future.<CompletableFuture<?>>handle((ignore, e) -> {
if (e == null) {
return future;
}
LOG.warning("Failed to fetch versions list and try to fetch from other source", e);
return refreshAsync(gameVersion, sourceIndex + 1);
}).thenCompose(it -> it);
}
}
@Override
public CompletableFuture<?> refreshAsync(String gameVersion) {
public Task<?> refreshAsync(String gameVersion) {
versions.clear(gameVersion);
return refreshAsync(gameVersion, 0);
}
@@ -17,17 +17,16 @@
*/
package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.SimpleMultimap;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* The remote version list.
*
* @param <T> The subclass of {@code RemoteVersion}, the type of RemoteVersion.
*
* @author huangyuhui
*/
public abstract class VersionList<T extends RemoteVersion> {
@@ -48,6 +47,7 @@ public abstract class VersionList<T extends RemoteVersion> {
/**
* True if the version list that contains the remote versions which depends on the specific game version has been loaded.
*
* @param gameVersion the remote version depends on
*/
public boolean isLoaded(String gameVersion) {
@@ -61,44 +61,36 @@ public abstract class VersionList<T extends RemoteVersion> {
/**
* @return the task to reload the remote version list.
*/
public abstract CompletableFuture<?> refreshAsync();
public abstract Task<?> refreshAsync();
/**
* @param gameVersion the remote version depends on
* @return the task to reload the remote version list.
*/
public CompletableFuture<?> refreshAsync(String gameVersion) {
public Task<?> refreshAsync(String gameVersion) {
return refreshAsync();
}
public CompletableFuture<?> loadAsync() {
return CompletableFuture.completedFuture(null)
.thenComposeAsync(unused -> {
lock.readLock().lock();
boolean loaded;
try {
loaded = isLoaded();
} finally {
lock.readLock().unlock();
}
return loaded ? CompletableFuture.completedFuture(null) : refreshAsync();
});
public Task<?> loadAsync() {
return Task.composeAsync(() -> {
lock.readLock().lock();
try {
return isLoaded() ? null : refreshAsync();
} finally {
lock.readLock().unlock();
}
});
}
public CompletableFuture<?> loadAsync(String gameVersion) {
return CompletableFuture.completedFuture(null)
.thenComposeAsync(unused -> {
lock.readLock().lock();
boolean loaded;
try {
loaded = isLoaded(gameVersion);
} finally {
lock.readLock().unlock();
}
return loaded ? CompletableFuture.completedFuture(null) : refreshAsync(gameVersion);
});
public Task<?> loadAsync(String gameVersion) {
return Task.composeAsync(() -> {
lock.readLock().lock();
try {
return isLoaded(gameVersion) ? null : refreshAsync(gameVersion);
} finally {
lock.readLock().unlock();
}
});
}
protected Collection<T> getVersionsImpl(String gameVersion) {
@@ -123,7 +115,7 @@ public abstract class VersionList<T extends RemoteVersion> {
/**
* Get the specific remote version.
*
* @param gameVersion the Minecraft version that remote versions belong to
* @param gameVersion the Minecraft version that remote versions belong to
* @param remoteVersion the version of the remote version.
* @return the specific remote version, null if it is not found.
*/
@@ -23,7 +23,7 @@ import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import java.io.IOException;
import java.net.URL;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -59,8 +59,8 @@ public final class FabricAPIInstallTask extends Task<Version> {
@Override
public void execute() throws IOException {
dependencies.add(new FileDownloadTask(
new URL(remote.getVersion().getFile().getUrl()),
dependencyManager.getGameRepository().getRunDirectory(version.getId()).toPath().resolve("mods").resolve("fabric-api-" + remote.getVersion().getVersion() + ".jar").toFile(),
URI.create(remote.getVersion().getFile().getUrl()),
dependencyManager.getGameRepository().getRunDirectory(version.getId()).toPath().resolve("mods").resolve("fabric-api-" + remote.getVersion().getVersion() + ".jar"),
remote.getVersion().getFile().getIntegrityCheck())
);
}
@@ -21,12 +21,10 @@ import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Lang;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;
import static org.jackhuang.hmcl.util.Lang.wrap;
public class FabricAPIVersionList extends VersionList<FabricAPIRemoteVersion> {
@@ -42,14 +40,14 @@ public class FabricAPIVersionList extends VersionList<FabricAPIRemoteVersion> {
}
@Override
public CompletableFuture<?> refreshAsync() {
return CompletableFuture.runAsync(wrap(() -> {
public Task<?> refreshAsync() {
return Task.runAsync(() -> {
for (RemoteMod.Version modVersion : Lang.toIterable(ModrinthRemoteModRepository.MODS.getRemoteVersionsById("P7dR8mSH"))) {
for (String gameVersion : modVersion.getGameVersions()) {
versions.put(gameVersion, new FabricAPIRemoteVersion(gameVersion, modVersion.getVersion(), modVersion.getName(), modVersion.getDatePublished(), modVersion,
Collections.singletonList(modVersion.getFile().getUrl())));
}
}
}));
});
}
}
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.download.fabric;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.Nullable;
@@ -26,10 +27,8 @@ import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.Lang.wrap;
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
public final class FabricVersionList extends VersionList<FabricRemoteVersion> {
@@ -45,8 +44,8 @@ public final class FabricVersionList extends VersionList<FabricRemoteVersion> {
}
@Override
public CompletableFuture<?> refreshAsync() {
return CompletableFuture.runAsync(wrap(() -> {
public Task<?> refreshAsync() {
return Task.runAsync(() -> {
List<String> gameVersions = getGameVersions(GAME_META_URL);
List<String> loaderVersions = getGameVersions(LOADER_META_URL);
@@ -60,7 +59,7 @@ public final class FabricVersionList extends VersionList<FabricRemoteVersion> {
} finally {
lock.writeLock().unlock();
}
}));
});
}
private static final String LOADER_META_URL = "https://meta.fabricmc.net/v2/versions/loader";
@@ -19,25 +19,25 @@ package org.jackhuang.hmcl.download.forge;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.net.URI;
import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.wrap;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -58,12 +58,12 @@ public final class ForgeBMCLVersionList extends VersionList<ForgeRemoteVersion>
}
@Override
public CompletableFuture<?> loadAsync() {
public Task<?> loadAsync() {
throw new UnsupportedOperationException("ForgeBMCLVersionList does not support loading the entire Forge remote version list.");
}
@Override
public CompletableFuture<?> refreshAsync() {
public Task<?> refreshAsync() {
throw new UnsupportedOperationException("ForgeBMCLVersionList does not support loading the entire Forge remote version list.");
}
@@ -83,11 +83,10 @@ public final class ForgeBMCLVersionList extends VersionList<ForgeRemoteVersion>
}
@Override
public CompletableFuture<?> refreshAsync(String gameVersion) {
public Task<?> refreshAsync(String gameVersion) {
String lookupVersion = toLookupVersion(gameVersion);
return CompletableFuture.completedFuture(null)
.thenApplyAsync(wrap(unused -> HttpRequest.GET(apiRoot + "/forge/minecraft/" + lookupVersion).getJson(listTypeOf(ForgeVersion.class))))
return new GetTask(URI.create(apiRoot + "/forge/minecraft/" + lookupVersion)).thenGetJsonAsync(listTypeOf(ForgeVersion.class))
.thenAcceptAsync(forgeVersions -> {
lock.writeLock().lock();
try {
@@ -69,7 +69,7 @@ public final class ForgeInstallTask extends Task<Version> {
dependent = new FileDownloadTask(
dependencyManager.getDownloadProvider().injectURLsWithCandidates(remote.getUrls()),
installer.toFile(), null);
installer, null);
dependent.setCacheRepository(dependencyManager.getCacheRepository());
dependent.setCaching(true);
dependent.addIntegrityCheckHandler(FileDownloadTask.ZIP_INTEGRITY_CHECK_HANDLER);
@@ -31,7 +31,6 @@ import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
@@ -48,7 +47,7 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -346,12 +345,12 @@ public class ForgeNewInstallTask extends Task<Version> {
throw new Exception("client_mappings download info not found");
}
List<URL> mappingsUrl = dependencyManager.getDownloadProvider()
List<URI> mappingsUrl = dependencyManager.getDownloadProvider()
.injectURLWithCandidates(mappings.getUrl());
FileDownloadTask mappingsTask = new FileDownloadTask(
var mappingsTask = new FileDownloadTask(
mappingsUrl,
new File(output),
IntegrityCheck.of("SHA-1", mappings.getSha1()));
Path.of(output),
FileDownloadTask.IntegrityCheck.of("SHA-1", mappings.getSha1()));
mappingsTask.setCaching(true);
mappingsTask.setCacheRepository(dependencyManager.getCacheRepository());
return mappingsTask;
@@ -19,14 +19,15 @@ package org.jackhuang.hmcl.download.forge;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.net.URI;
import java.time.Instant;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
/**
*
@@ -53,8 +54,8 @@ public final class ForgeVersionList extends VersionList<ForgeRemoteVersion> {
}
@Override
public CompletableFuture<?> refreshAsync() {
return HttpRequest.GET(FORGE_LIST).getJsonAsync(ForgeVersionRoot.class)
public Task<?> refreshAsync() {
return new GetTask(FORGE_LIST).thenGetJsonAsync(ForgeVersionRoot.class)
.thenAcceptAsync(root -> {
lock.writeLock().lock();
@@ -95,5 +96,5 @@ public final class ForgeVersionList extends VersionList<ForgeRemoteVersion> {
});
}
public static final String FORGE_LIST = "https://hmcl-dev.github.io/metadata/forge/";
public static final URI FORGE_LIST = URI.create("https://hmcl-dev.github.io/metadata/forge/");
}
@@ -29,7 +29,7 @@ import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.IOException;
import java.net.URL;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
@@ -102,9 +102,9 @@ public final class GameAssetDownloadTask extends Task<Void> {
LOG.warning("Unable to calc hash value of file " + file, e);
}
if (download) {
List<URL> urls = dependencyManager.getDownloadProvider().getAssetObjectCandidates(assetObject.getLocation());
List<URI> uris = dependencyManager.getDownloadProvider().getAssetObjectCandidates(assetObject.getLocation());
FileDownloadTask task = new FileDownloadTask(urls, file.toFile(), new FileDownloadTask.IntegrityCheck("SHA-1", assetObject.getHash()));
var task = new FileDownloadTask(uris, file, new FileDownloadTask.IntegrityCheck("SHA-1", assetObject.getHash()));
task.setName(assetObject.getHash());
task.setCandidate(dependencyManager.getCacheRepository().getCommonDirectory()
.resolve("assets").resolve("objects").resolve(assetObject.getLocation()));
@@ -94,9 +94,9 @@ public final class GameAssetIndexDownloadTask extends Task<Void> {
// We should not check the hash code of asset index file since this file is not consistent
// And Mojang will modify this file anytime. So assetIndex.hash might be outdated.
FileDownloadTask task = new FileDownloadTask(
var task = new FileDownloadTask(
dependencyManager.getDownloadProvider().injectURLWithCandidates(assetIndexInfo.getUrl()),
assetIndexFile.toFile(),
assetIndexFile,
verifyHashCode ? new FileDownloadTask.IntegrityCheck("SHA-1", assetIndexInfo.getSha1()) : null
);
task.setCacheRepository(dependencyManager.getCacheRepository());
@@ -20,11 +20,10 @@ package org.jackhuang.hmcl.download.game;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.CacheRepository;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -54,12 +53,12 @@ public final class GameDownloadTask extends Task<Void> {
@Override
public void execute() {
File jar = dependencyManager.getGameRepository().getVersionJar(version);
Path jar = dependencyManager.getGameRepository().getVersionJar(version).toPath();
FileDownloadTask task = new FileDownloadTask(
var task = new FileDownloadTask(
dependencyManager.getDownloadProvider().injectURLWithCandidates(version.getDownloadInfo().getUrl()),
jar,
IntegrityCheck.of(CacheRepository.SHA1, version.getDownloadInfo().getSha1()));
FileDownloadTask.IntegrityCheck.of(CacheRepository.SHA1, version.getDownloadInfo().getSha1()));
task.setCaching(true);
task.setCacheRepository(dependencyManager.getCacheRepository());
@@ -19,14 +19,15 @@ package org.jackhuang.hmcl.download.game;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.HttpRequest;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URI;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -52,8 +53,8 @@ public final class GameVersionList extends VersionList<GameRemoteVersion> {
}
@Override
public CompletableFuture<?> refreshAsync() {
return HttpRequest.GET(downloadProvider.getVersionListURL()).getJsonAsync(GameRemoteVersions.class)
public Task<?> refreshAsync() {
return new GetTask(URI.create(downloadProvider.getVersionListURL())).thenGetJsonAsync(GameRemoteVersions.class)
.thenAcceptAsync(root -> {
GameRemoteVersions unlistedVersions = null;
@@ -30,7 +30,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -116,8 +116,8 @@ public class LibraryDownloadTask extends Task<Void> {
}
List<URL> urls = dependencyManager.getDownloadProvider().injectURLWithCandidates(url);
task = new FileDownloadTask(urls, jar,
List<URI> uris = dependencyManager.getDownloadProvider().injectURLWithCandidates(url);
task = new FileDownloadTask(uris, jar.toPath(),
library.getDownload().getSha1() != null ? new IntegrityCheck("SHA-1", library.getDownload().getSha1()) : null);
task.setCacheRepository(cacheRepository);
task.setCaching(true);
@@ -44,7 +44,7 @@ public final class VersionJsonDownloadTask extends Task<String> {
this.dependencyManager = dependencyManager;
this.gameVersionList = dependencyManager.getVersionList("game");
dependents.add(Task.fromCompletableFuture(gameVersionList.loadAsync(gameVersion)));
dependents.add(gameVersionList.loadAsync(gameVersion));
setSignificance(TaskSignificance.MODERATE);
}
@@ -21,9 +21,9 @@ import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* This task is to save the version json.
@@ -51,7 +51,8 @@ public final class VersionJsonSaveTask extends Task<Version> {
@Override
public void execute() throws Exception {
File json = repository.getVersionJson(version.getId()).getAbsoluteFile();
FileUtils.writeText(json, JsonUtils.GSON.toJson(version));
Path json = repository.getVersionJson(version.getId()).toPath().toAbsolutePath();
Files.createDirectories(json.getParent());
JsonUtils.writeToJsonFile(json, version);
}
}
@@ -103,7 +103,7 @@ public final class MojangJavaDownloadTask extends Task<MojangJavaDownloadTask.Re
if (file.getDownloads().containsKey("lzma")) {
DownloadInfo download = file.getDownloads().get("lzma");
File tempFile = target.resolve(entry.getKey() + ".lzma").toFile();
FileDownloadTask task = new FileDownloadTask(downloadProvider.injectURLWithCandidates(download.getUrl()), tempFile, new FileDownloadTask.IntegrityCheck("SHA-1", download.getSha1()));
var task = new FileDownloadTask(downloadProvider.injectURLWithCandidates(download.getUrl()), tempFile.toPath(), new FileDownloadTask.IntegrityCheck("SHA-1", download.getSha1()));
task.setName(entry.getKey());
dependencies.add(task.thenRunAsync(() -> {
Path decompressed = target.resolve(entry.getKey() + ".tmp");
@@ -121,7 +121,7 @@ public final class MojangJavaDownloadTask extends Task<MojangJavaDownloadTask.Re
}));
} else if (file.getDownloads().containsKey("raw")) {
DownloadInfo download = file.getDownloads().get("raw");
FileDownloadTask task = new FileDownloadTask(downloadProvider.injectURLWithCandidates(download.getUrl()), dest.toFile(), new FileDownloadTask.IntegrityCheck("SHA-1", download.getSha1()));
var task = new FileDownloadTask(downloadProvider.injectURLWithCandidates(download.getUrl()), dest, new FileDownloadTask.IntegrityCheck("SHA-1", download.getSha1()));
task.setName(entry.getKey());
if (file.isExecutable()) {
dependencies.add(task.thenRunAsync(() -> dest.toFile().setExecutable(true)));
@@ -20,12 +20,12 @@ package org.jackhuang.hmcl.download.liteloader;
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;
import java.util.Map;
/**
* @author huangyuhui
@@ -54,17 +54,15 @@ public final class LiteLoaderBMCLVersionList extends VersionList<LiteLoaderRemot
}
@Override
public CompletableFuture<?> refreshAsync() {
public Task<?> refreshAsync() {
throw new UnsupportedOperationException();
}
@Override
public CompletableFuture<?> refreshAsync(String gameVersion) {
return HttpRequest.GET(
downloadProvider.injectURL("https://bmclapi2.bangbang93.com/liteloader/list"), Pair.pair("mcversion", gameVersion)
)
.getJsonAsync(LiteLoaderBMCLVersion.class)
.thenAccept(v -> {
public Task<?> refreshAsync(String gameVersion) {
return new GetTask(NetworkUtils.withQuery(downloadProvider.injectURLWithCandidates("https://bmclapi2.bangbang93.com/liteloader/list"), Map.of("mcversion", gameVersion)))
.thenGetJsonAsync(LiteLoaderBMCLVersion.class)
.thenAcceptAsync(v -> {
lock.writeLock().lock();
try {
versions.clear();
@@ -20,16 +20,18 @@ package org.jackhuang.hmcl.download.liteloader;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
/**
* @author huangyuhui
@@ -50,8 +52,9 @@ public final class LiteLoaderVersionList extends VersionList<LiteLoaderRemoteVer
public static final String LITELOADER_LIST = "https://dl.liteloader.com/versions/versions.json";
@Override
public CompletableFuture<?> refreshAsync(String gameVersion) {
return HttpRequest.GET(downloadProvider.injectURL(LITELOADER_LIST)).getJsonAsync(LiteLoaderVersionsRoot.class)
public Task<?> refreshAsync(String gameVersion) {
return new GetTask(URI.create(downloadProvider.injectURL(LITELOADER_LIST)))
.thenGetJsonAsync(LiteLoaderVersionsRoot.class)
.thenAcceptAsync(root -> {
LiteLoaderGameVersions versions = root.getVersions().get(gameVersion);
if (versions == null) {
@@ -85,7 +88,7 @@ public final class LiteLoaderVersionList extends VersionList<LiteLoaderRemoteVer
}
@Override
public CompletableFuture<?> refreshAsync() {
public Task<?> refreshAsync() {
throw new UnsupportedOperationException();
}
@@ -20,15 +20,15 @@ package org.jackhuang.hmcl.download.neoforge;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.HttpRequest;
import java.net.URI;
import java.util.Collections;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import static org.jackhuang.hmcl.util.Lang.wrap;
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
public final class NeoForgeBMCLVersionList extends VersionList<NeoForgeRemoteVersion> {
@@ -47,12 +47,12 @@ public final class NeoForgeBMCLVersionList extends VersionList<NeoForgeRemoteVer
}
@Override
public CompletableFuture<?> loadAsync() {
public Task<?> loadAsync() {
throw new UnsupportedOperationException("NeoForgeBMCLVersionList does not support loading the entire NeoForge remote version list.");
}
@Override
public CompletableFuture<?> refreshAsync() {
public Task<?> refreshAsync() {
throw new UnsupportedOperationException("NeoForgeBMCLVersionList does not support loading the entire NeoForge remote version list.");
}
@@ -65,9 +65,8 @@ public final class NeoForgeBMCLVersionList extends VersionList<NeoForgeRemoteVer
}
@Override
public CompletableFuture<?> refreshAsync(String gameVersion) {
return CompletableFuture.completedFuture((Void) null)
.thenApplyAsync(wrap(unused -> HttpRequest.GET(apiRoot + "/neoforge/list/" + gameVersion).getJson(listTypeOf(NeoForgeVersion.class))))
public Task<?> refreshAsync(String gameVersion) {
return new GetTask(URI.create(apiRoot + "/neoforge/list/" + gameVersion)).thenGetJsonAsync(listTypeOf(NeoForgeVersion.class))
.thenAcceptAsync(neoForgeVersions -> {
lock.writeLock().lock();
@@ -49,7 +49,7 @@ public final class NeoForgeInstallTask extends Task<Version> {
dependent = new FileDownloadTask(
dependencyManager.getDownloadProvider().injectURLsWithCandidates(remoteVersion.getUrls()),
installer.toFile(), null
installer, null
);
dependent.setCacheRepository(dependencyManager.getCacheRepository());
dependent.setCaching(true);
@@ -2,14 +2,14 @@ package org.jackhuang.hmcl.download.neoforge;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import static org.jackhuang.hmcl.util.Lang.wrap;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class NeoForgeOfficialVersionList extends VersionList<NeoForgeRemoteVersion> {
@@ -37,17 +37,17 @@ public final class NeoForgeOfficialVersionList extends VersionList<NeoForgeRemot
}
@Override
public CompletableFuture<?> refreshAsync() {
return CompletableFuture.supplyAsync(wrap(() -> new OfficialAPIResult[]{
HttpRequest.GET(downloadProvider.injectURL(OLD_URL)).getJson(OfficialAPIResult.class),
HttpRequest.GET(downloadProvider.injectURL(META_URL)).getJson(OfficialAPIResult.class)
})).thenAccept(results -> {
public Task<?> refreshAsync() {
return Task.allOf(
new GetTask(URI.create(downloadProvider.injectURL(OLD_URL))).thenGetJsonAsync(OfficialAPIResult.class),
new GetTask(URI.create(downloadProvider.injectURL(META_URL))).thenGetJsonAsync(OfficialAPIResult.class)
).thenAcceptAsync(results -> {
lock.writeLock().lock();
try {
versions.clear();
for (String version : results[0].versions) {
for (String version : results.get(0).versions) {
versions.put("1.20.1", new NeoForgeRemoteVersion(
"1.20.1", NeoForgeRemoteVersion.normalize(version),
Collections.singletonList(
@@ -56,7 +56,7 @@ public final class NeoForgeOfficialVersionList extends VersionList<NeoForgeRemot
));
}
for (String version : results[1].versions) {
for (String version : results.get(1).versions) {
String mcVersion;
try {
@@ -26,7 +26,6 @@ import org.jackhuang.hmcl.download.game.GameLibrariesTask;
import org.jackhuang.hmcl.download.game.VersionJsonDownloadTask;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.StringUtils;
@@ -44,7 +43,7 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -342,12 +341,12 @@ public class NeoForgeOldInstallTask extends Task<Version> {
throw new Exception("client_mappings download info not found");
}
List<URL> mappingsUrl = dependencyManager.getDownloadProvider()
List<URI> mappingsUri = dependencyManager.getDownloadProvider()
.injectURLWithCandidates(mappings.getUrl());
FileDownloadTask mappingsTask = new FileDownloadTask(
mappingsUrl,
new File(output),
IntegrityCheck.of("SHA-1", mappings.getSha1()));
var mappingsTask = new FileDownloadTask(
mappingsUri,
Path.of(output),
FileDownloadTask.IntegrityCheck.of("SHA-1", mappings.getSha1()));
mappingsTask.setCaching(true);
mappingsTask.setCacheRepository(dependencyManager.getCacheRepository());
return mappingsTask;
@@ -19,14 +19,15 @@ package org.jackhuang.hmcl.download.optifine;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.net.URI;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
@@ -71,8 +72,8 @@ public final class OptiFineBMCLVersionList extends VersionList<OptiFineRemoteVer
}
@Override
public CompletableFuture<?> refreshAsync() {
return HttpRequest.GET(apiRoot + "/optifine/versionlist").getJsonAsync(listTypeOf(OptiFineVersion.class)).thenAcceptAsync(root -> {
public Task<?> refreshAsync() {
return new GetTask(URI.create(apiRoot + "/optifine/versionlist")).thenGetJsonAsync(listTypeOf(OptiFineVersion.class)).thenAcceptAsync(root -> {
lock.writeLock().lock();
try {
@@ -96,9 +96,9 @@ public final class OptiFineInstallTask extends Task<Version> {
dest = Files.createTempFile("optifine-installer", ".jar");
if (installer == null) {
FileDownloadTask task = new FileDownloadTask(
var task = new FileDownloadTask(
dependencyManager.getDownloadProvider().injectURLsWithCandidates(remote.getUrls()),
dest.toFile(), null);
dest, null);
task.setCacheRepository(dependencyManager.getCacheRepository());
task.setCaching(true);
dependents.add(task);
@@ -23,7 +23,7 @@ import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import java.io.IOException;
import java.net.URL;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -59,8 +59,8 @@ public final class QuiltAPIInstallTask extends Task<Version> {
@Override
public void execute() throws IOException {
dependencies.add(new FileDownloadTask(
new URL(remote.getVersion().getFile().getUrl()),
dependencyManager.getGameRepository().getRunDirectory(version.getId()).toPath().resolve("mods").resolve("quilt-api-" + remote.getVersion().getVersion() + ".jar").toFile(),
URI.create(remote.getVersion().getFile().getUrl()),
dependencyManager.getGameRepository().getRunDirectory(version.getId()).toPath().resolve("mods").resolve("quilt-api-" + remote.getVersion().getVersion() + ".jar"),
remote.getVersion().getFile().getIntegrityCheck())
);
}
@@ -21,12 +21,10 @@ import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Lang;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;
import static org.jackhuang.hmcl.util.Lang.wrap;
public class QuiltAPIVersionList extends VersionList<QuiltAPIRemoteVersion> {
@@ -42,14 +40,14 @@ public class QuiltAPIVersionList extends VersionList<QuiltAPIRemoteVersion> {
}
@Override
public CompletableFuture<?> refreshAsync() {
return CompletableFuture.runAsync(wrap(() -> {
public Task<?> refreshAsync() {
return Task.runAsync(() -> {
for (RemoteMod.Version modVersion : Lang.toIterable(ModrinthRemoteModRepository.MODS.getRemoteVersionsById("qsl"))) {
for (String gameVersion : modVersion.getGameVersions()) {
versions.put(gameVersion, new QuiltAPIRemoteVersion(gameVersion, modVersion.getVersion(), modVersion.getName(), modVersion.getDatePublished(), modVersion,
Collections.singletonList(modVersion.getFile().getUrl())));
}
}
}));
});
}
}
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.download.quilt;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.Nullable;
@@ -26,10 +27,8 @@ import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.Lang.wrap;
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
public final class QuiltVersionList extends VersionList<QuiltRemoteVersion> {
@@ -45,8 +44,8 @@ public final class QuiltVersionList extends VersionList<QuiltRemoteVersion> {
}
@Override
public CompletableFuture<?> refreshAsync() {
return CompletableFuture.runAsync(wrap(() -> {
public Task<?> refreshAsync() {
return Task.runAsync(() -> {
List<String> gameVersions = getGameVersions(GAME_META_URL);
List<String> loaderVersions = getGameVersions(LOADER_META_URL);
@@ -60,7 +59,7 @@ public final class QuiltVersionList extends VersionList<QuiltRemoteVersion> {
} finally {
lock.writeLock().unlock();
}
}));
});
}
private static final String LOADER_META_URL = "https://meta.quiltmc.org/v3/versions/loader";
@@ -219,8 +219,9 @@ public class DefaultGameRepository implements GameRepository {
// fix inheritsFrom of versions that inherits from version [from].
for (Version version : getVersions()) {
if (from.equals(version.getInheritsFrom())) {
File json = getVersionJson(version.getId()).getAbsoluteFile();
FileUtils.writeText(json, JsonUtils.GSON.toJson(version.setInheritsFrom(to)));
Path targetPath = getVersionJson(version.getId()).toPath();
Files.createDirectories(targetPath.getParent());
JsonUtils.writeToJsonFile(targetPath, version.setInheritsFrom(to));
}
}
return true;
@@ -384,7 +385,7 @@ public class DefaultGameRepository implements GameRepository {
@Override
public AssetIndex getAssetIndex(String version, String assetId) throws IOException {
try {
return Objects.requireNonNull(JsonUtils.GSON.fromJson(Files.readString(getIndexFile(version, assetId)), AssetIndex.class));
return Objects.requireNonNull(JsonUtils.fromJsonFile(getIndexFile(version, assetId), AssetIndex.class));
} catch (JsonParseException | NullPointerException e) {
throw new IOException("Asset index file malformed", e);
}
@@ -445,8 +446,7 @@ public class DefaultGameRepository implements GameRepository {
if (!Files.isRegularFile(indexFile))
return assetsDir;
String assetIndexContent = Files.readString(indexFile);
AssetIndex index = JsonUtils.GSON.fromJson(assetIndexContent, AssetIndex.class);
AssetIndex index = JsonUtils.fromJsonFile(indexFile, AssetIndex.class);
if (index == null)
return assetsDir;
@@ -455,7 +455,7 @@ public class DefaultGameRepository implements GameRepository {
Path resourcesDir = getRunDirectory(version).toPath().resolve("resources");
int cnt = 0;
int tot = index.getObjects().entrySet().size();
int tot = index.getObjects().size();
for (Map.Entry<String, AssetObject> entry : index.getObjects().entrySet()) {
Path target = virtualRoot.resolve(entry.getKey());
Path original = getAssetObject(version, assetsDir, entry.getValue());
@@ -512,7 +512,7 @@ public class DefaultGameRepository implements GameRepository {
if (!hasVersion(version)) throw new VersionNotFoundException(version);
File file = getModpackConfiguration(version);
if (!file.exists()) return null;
return JsonUtils.GSON.fromJson(Files.readString(file.toPath()), ModpackConfiguration.class);
return JsonUtils.fromJsonFile(file.toPath(), ModpackConfiguration.class);
}
public boolean isModpack(String version) {
@@ -53,7 +53,8 @@ public class Library implements Comparable<Library>, Validation {
"",
Architecture.SYSTEM_ARCH.name().toLowerCase(Locale.ROOT),
Architecture.SYSTEM_ARCH.getBits().getBit()
}, variants = {"", "native", "natives"};
};
String[] variants = {"", "native", "natives"};
POSSIBLE_NATIVE_DESCRIPTORS = new String[keys.length * variants.length];
StringBuilder builder = new StringBuilder();
@@ -132,7 +132,7 @@ public class Datapack {
try {
PackMcMeta pack = JsonUtils.fromNonNullJson(Files.readString(mcmeta), PackMcMeta.class);
Platform.runLater(() -> info.add(new Pack(path, FileUtils.getNameWithoutExtension(path), pack.getPackInfo().getDescription(), this)));
} catch (IOException | JsonParseException e) {
} catch (Exception e) {
LOG.warning("Failed to read datapack " + path, e);
}
} else {
@@ -144,7 +144,7 @@ public class Datapack {
public void loadFromDir() {
try {
loadFromDir(path);
} catch (IOException e) {
} catch (Exception e) {
LOG.warning("Failed to read datapacks " + path, e);
}
}
@@ -75,7 +75,9 @@ public final class MinecraftInstanceTask<T> extends Task<ModpackConfiguration<T>
}
ModpackConfiguration<T> configuration = new ModpackConfiguration<>(manifest, type, name, version, overrides);
FileUtils.writeText(jsonFile, JsonUtils.GSON.toJson(configuration));
Path jsonPath = jsonFile.toPath();
Files.createDirectories(jsonPath.getParent());
JsonUtils.writeToJsonFile(jsonPath, configuration);
setResult(configuration);
}
}
@@ -27,12 +27,10 @@ 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.FileUtils;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -88,7 +86,7 @@ public final class CurseCompletionTask extends Task<Void> {
try {
File manifestFile = new File(repository.getVersionRoot(version), "manifest.json");
if (manifestFile.exists())
this.manifest = JsonUtils.GSON.fromJson(Files.readString(manifestFile.toPath()), CurseManifest.class);
this.manifest = JsonUtils.fromJsonFile(manifestFile.toPath(), CurseManifest.class);
} catch (Exception e) {
LOG.warning("Unable to read CurseForge modpack manifest.json", e);
}
@@ -137,7 +135,7 @@ public final class CurseCompletionTask extends Task<Void> {
}
})
.collect(Collectors.toList()));
FileUtils.writeText(new File(root, "manifest.json"), JsonUtils.GSON.toJson(newManifest));
JsonUtils.writeToJsonFile(root.toPath().resolve("manifest.json"), newManifest);
File versionRoot = repository.getVersionRoot(modManager.getVersion());
File resourcePacksRoot = new File(versionRoot, "resourcepacks"), shaderPacksRoot = new File(versionRoot, "shaderpacks");
@@ -152,7 +150,7 @@ public final class CurseCompletionTask extends Task<Void> {
return Stream.empty();
}
FileDownloadTask task = new FileDownloadTask(f.getUrl(), path);
var task = new FileDownloadTask(f.getUrl(), path.toPath());
task.setCacheRepository(dependency.getCacheRepository());
task.setCaching(true);
return Stream.of(task.withCounter("hmcl.modpack.download"));
@@ -25,11 +25,11 @@ import org.jackhuang.hmcl.mod.*;
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.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -60,7 +60,6 @@ public final class CurseInstallTask extends Task<Void> {
* @param zipFile the CurseForge modpack file.
* @param manifest The manifest content of given CurseForge modpack.
* @param name the new version name
* @see CurseManifest#readCurseForgeModpackManifest
*/
public CurseInstallTask(DefaultDependencyManager dependencyManager, File zipFile, Modpack modpack, CurseManifest manifest, String name) {
this.dependencyManager = dependencyManager;
@@ -99,7 +98,7 @@ public final class CurseInstallTask extends Task<Void> {
ModpackConfiguration<CurseManifest> config = null;
try {
if (json.exists()) {
config = JsonUtils.GSON.fromJson(Files.readString(json.toPath()), ModpackConfiguration.typeOf(CurseManifest.class));
config = JsonUtils.fromJsonFile(json.toPath(), ModpackConfiguration.typeOf(CurseManifest.class));
if (!CurseModpackProvider.INSTANCE.getName().equals(config.getType()))
throw new IllegalArgumentException("Version " + name + " is not a Curse modpack. Cannot update this version.");
@@ -137,7 +136,8 @@ public final class CurseInstallTask extends Task<Void> {
}
}
File root = repository.getVersionRoot(name);
FileUtils.writeText(new File(root, "manifest.json"), JsonUtils.GSON.toJson(manifest));
Path root = repository.getVersionRoot(name).toPath();
Files.createDirectories(root);
JsonUtils.writeToJsonFile(root.resolve("manifest.json"), manifest);
}
}
@@ -24,7 +24,7 @@ import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.Nullable;
import java.net.URL;
import java.net.URI;
import java.util.Objects;
/**
@@ -84,15 +84,15 @@ public final class CurseManifestFile implements Validation {
}
@Nullable
public URL getUrl() {
public URI getUrl() {
if (url == null) {
if (fileName != null) {
return NetworkUtils.toURL(NetworkUtils.encodeLocation(String.format("https://edge.forgecdn.net/files/%d/%d/%s", fileID / 1000, fileID % 1000, fileName)));
return URI.create(NetworkUtils.encodeLocation(String.format("https://edge.forgecdn.net/files/%d/%d/%s", fileID / 1000, fileID % 1000, fileName)));
} else {
return null;
}
} else {
return NetworkUtils.toURL(NetworkUtils.encodeLocation(url));
return URI.create(NetworkUtils.encodeLocation(url));
}
}
@@ -28,7 +28,6 @@ import org.jackhuang.hmcl.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.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -36,7 +35,7 @@ import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
@@ -102,7 +101,7 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
throw new CustomException();
}
})).thenComposeAsync(wrap(unused1 -> {
return executor.one(new GetTask(new URL(manifest.getFileApi() + "/manifest.json")));
return executor.one(new GetTask(URI.create(manifest.getFileApi() + "/manifest.json")));
})).thenComposeAsync(wrap(remoteManifestJson -> {
McbbsModpackManifest remoteManifest;
// We needs to update modpack from online server.
@@ -113,6 +112,7 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
}
Path rootPath = repository.getVersionRoot(version).toPath();
Files.createDirectories(rootPath);
Map<McbbsModpackManifest.File, McbbsModpackManifest.File> localFiles = manifest.getFiles().stream().collect(Collectors.toMap(Function.identity(), Function.identity()));
@@ -173,15 +173,15 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
manifest = remoteManifest.setFiles(newFiles);
return executor.all(tasks.stream().filter(Objects::nonNull).collect(Collectors.toList()));
})).thenAcceptAsync(wrapConsumer(unused1 -> {
File manifestFile = repository.getModpackConfiguration(version);
FileUtils.writeText(manifestFile, JsonUtils.GSON.toJson(
Path manifestFile = repository.getModpackConfiguration(version).toPath();
JsonUtils.writeToJsonFile(manifestFile,
new ModpackConfiguration<>(manifest, this.configuration.getType(), this.manifest.getName(), this.manifest.getVersion(),
this.manifest.getFiles().stream()
.flatMap(file -> file instanceof McbbsModpackManifest.AddonFile
? Stream.of((McbbsModpackManifest.AddonFile) file)
: Stream.empty())
.map(file -> new ModpackConfiguration.FileInformation(file.getPath(), file.getHash()))
.collect(Collectors.toList()))));
.collect(Collectors.toList())));
})));
}).thenComposeAsync(unused -> {
AtomicBoolean allNameKnown = new AtomicBoolean(true);
@@ -204,7 +204,7 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
return file.withFileName(NetworkUtils.detectFileName(file.getUrl()));
} catch (IOException e) {
try {
String result = NetworkUtils.doGet(NetworkUtils.toURL(String.format("https://cursemeta.dries007.net/%d/%d.json", file.getProjectID(), file.getFileID())));
String result = NetworkUtils.doGet(URI.create(String.format("https://cursemeta.dries007.net/%d/%d.json", file.getProjectID(), file.getFileID())));
CurseMetaMod mod = JsonUtils.fromNonNullJson(result, CurseMetaMod.class);
return file.withFileName(mod.getFileNameOnDisk()).withURL(mod.getDownloadURL());
} catch (FileNotFoundException fof) {
@@ -213,7 +213,7 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
return file;
} catch (IOException | JsonParseException e2) {
try {
String result = NetworkUtils.doGet(NetworkUtils.toURL(String.format("https://addons-ecs.forgesvc.net/api/v2/addon/%d/file/%d", file.getProjectID(), file.getFileID())));
String result = NetworkUtils.doGet(URI.create(String.format("https://addons-ecs.forgesvc.net/api/v2/addon/%d/file/%d", file.getProjectID(), file.getFileID())));
CurseMetaMod mod = JsonUtils.fromNonNullJson(result, CurseMetaMod.class);
return file.withFileName(mod.getFileName()).withURL(mod.getDownloadURL());
} catch (FileNotFoundException fof) {
@@ -240,14 +240,14 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
manifest = newManifest;
configuration = configuration.setManifest(newManifest);
FileUtils.writeText(configurationFile, JsonUtils.GSON.toJson(configuration));
JsonUtils.writeToJsonFile(configurationFile.toPath(), configuration);
for (McbbsModpackManifest.File file : newManifest.getFiles())
if (file instanceof McbbsModpackManifest.CurseFile) {
McbbsModpackManifest.CurseFile curseFile = (McbbsModpackManifest.CurseFile) file;
if (StringUtils.isNotBlank(curseFile.getFileName())) {
if (!modManager.hasSimpleMod(curseFile.getFileName())) {
FileDownloadTask task = new FileDownloadTask(curseFile.getUrl(), modManager.getSimpleModPath(curseFile.getFileName()).toFile());
var task = new FileDownloadTask(curseFile.getUrl(), modManager.getSimpleModPath(curseFile.getFileName()));
task.setCacheRepository(dependency.getCacheRepository());
task.setCaching(true);
dependencies.add(task.withCounter("hmcl.modpack.download"));
@@ -297,8 +297,8 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
if (file instanceof McbbsModpackManifest.AddonFile) {
McbbsModpackManifest.AddonFile addonFile = (McbbsModpackManifest.AddonFile) file;
return new FileDownloadTask(
new URL(remoteManifest.getFileApi() + "/overrides/" + NetworkUtils.encodeLocation(addonFile.getPath())),
modManager.getSimpleModPath(addonFile.getPath()).toFile(),
URI.create(remoteManifest.getFileApi() + "/overrides/" + NetworkUtils.encodeLocation(addonFile.getPath())),
modManager.getSimpleModPath(addonFile.getPath()),
addonFile.getHash() != null ? new FileDownloadTask.IntegrityCheck("SHA-1", addonFile.getHash()) : null);
} else if (file instanceof McbbsModpackManifest.CurseFile) {
// we download it later.
@@ -31,7 +31,6 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -79,7 +78,7 @@ public class McbbsModpackLocalInstallTask extends Task<Void> {
ModpackConfiguration<McbbsModpackManifest> config = null;
try {
if (json.exists()) {
config = JsonUtils.GSON.fromJson(Files.readString(json.toPath()), ModpackConfiguration.typeOf(McbbsModpackManifest.class));
config = JsonUtils.fromJsonFile(json.toPath(), ModpackConfiguration.typeOf(McbbsModpackManifest.class));
if (!McbbsModpackProvider.INSTANCE.getName().equals(config.getType()))
throw new IllegalArgumentException("Version " + name + " is not a Mcbbs modpack. Cannot update this version.");
@@ -31,7 +31,7 @@ import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.net.URL;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.List;
@@ -329,9 +329,9 @@ public class McbbsModpackManifest implements ModpackManifest, Validation {
return fileName;
}
public URL getUrl() {
return url == null ? NetworkUtils.toURL("https://www.curseforge.com/minecraft/mc-mods/" + projectID + "/download/" + fileID + "/file")
: NetworkUtils.toURL(NetworkUtils.encodeLocation(url));
public URI getUrl() {
return url == null ? URI.create("https://www.curseforge.com/minecraft/mc-mods/" + projectID + "/download/" + fileID + "/file")
: URI.create(NetworkUtils.encodeLocation(url));
}
public CurseFile withFileName(String fileName) {
@@ -27,7 +27,6 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -62,10 +61,10 @@ public class McbbsModpackRemoteInstallTask extends Task<Void> {
repository.removeVersionFromDisk(name);
});
ModpackConfiguration<McbbsModpackManifest> config = null;
ModpackConfiguration<McbbsModpackManifest> config;
try {
if (json.exists()) {
config = JsonUtils.GSON.fromJson(Files.readString(json.toPath()), ModpackConfiguration.typeOf(McbbsModpackManifest.class));
config = JsonUtils.fromJsonFile(json.toPath(), ModpackConfiguration.typeOf(McbbsModpackManifest.class));
if (!MODPACK_TYPE.equals(config.getType()))
throw new IllegalArgumentException("Version " + name + " is not a Mcbbs modpack. Cannot update this version.");
@@ -125,7 +125,7 @@ public final class ForgeOldModMetadata {
Path mcmod = fs.getPath("mcmod.info");
if (Files.notExists(mcmod))
throw new IOException("File " + modFile + " is not a Forge mod.");
List<ForgeOldModMetadata> modList = JsonUtils.GSON.fromJson(Files.readString(mcmod), listTypeOf(ForgeOldModMetadata.class));
List<ForgeOldModMetadata> modList = JsonUtils.fromJsonFile(mcmod, listTypeOf(ForgeOldModMetadata.class));
if (modList == null || modList.isEmpty())
throw new IOException("Mod " + modFile + " `mcmod.info` is malformed..");
ForgeOldModMetadata metadata = modList.get(0);
Loaded 100 of 140 files, more files were not shown because too many files have changed in this diff. Show more